diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index cd9799dfa..279ca970b 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -20,6 +20,7 @@ import ( "bytes" "compress/gzip" "encoding/json" + "math" "mime" "net/http" "sync" @@ -54,6 +55,10 @@ type OpenAPIService struct { // rwMutex protects All members of this service. rwMutex sync.RWMutex + // eagerMarshalingTimer fires if no updates to the spec are made + // for the defined duration. + eagerMarshalingTimer *time.Timer + lastModified time.Time jsonCache handler.HandlerCache @@ -69,7 +74,9 @@ func init() { // NewOpenAPIService builds an OpenAPIService starting with the given spec. func NewOpenAPIService(spec *spec.Swagger) (*OpenAPIService, error) { o := &OpenAPIService{} + o.eagerMarshalingTimer = buildEagerMarshalingTimer(o) if err := o.UpdateSpec(spec); err != nil { + _ = o.eagerMarshalingTimer.Stop() return nil, err } return o, nil @@ -110,6 +117,8 @@ func (o *OpenAPIService) UpdateSpec(openapiSpec *spec.Swagger) (err error) { }) o.lastModified = time.Now() + _ = o.eagerMarshalingTimer.Reset(handler.EagerMarshalingCoolDown) + return nil } @@ -256,3 +265,12 @@ func BuildAndRegisterOpenAPIVersionedServiceFromRoutes(servePath string, routeCo } return o, o.RegisterOpenAPIVersionedService(servePath, handler) } + +func buildEagerMarshalingTimer(o *OpenAPIService) *time.Timer { + return time.AfterFunc( + time.Duration(math.MaxInt64), // placeholder duration, will be reset later + func() { + _, _, _ = o.jsonCache.Get() + _, _, _ = o.protoCache.Get() + }) +} diff --git a/pkg/handler/handler_test.go b/pkg/handler/handler_test.go index eaea1e320..6321e4a17 100644 --- a/pkg/handler/handler_test.go +++ b/pkg/handler/handler_test.go @@ -9,9 +9,11 @@ import ( "reflect" "sort" "testing" + "time" "github.com/davecgh/go-spew/spew" yaml "gopkg.in/yaml.v2" + "k8s.io/kube-openapi/pkg/internal/handler" "k8s.io/kube-openapi/pkg/validation/spec" ) @@ -177,3 +179,36 @@ func TestToProtoBinary(t *testing.T) { } // TODO: add some kind of roundtrip test here } + +func TestEagerMarshaling(t *testing.T) { + jsonCacheLoaded := false + protoCacheLoaded := false + o := &OpenAPIService{} + o.eagerMarshalingTimer = buildEagerMarshalingTimer(o) + o.jsonCache = handler.HandlerCache{BuildCache: func() ([]byte, error) { + jsonCacheLoaded = true + return nil, nil + }} + o.protoCache = handler.HandlerCache{ + BuildCache: func() ([]byte, error) { + protoCacheLoaded = true + return nil, nil + }, + } + // kick off the eager marshalling timer + _ = o.eagerMarshalingTimer.Reset(time.Microsecond) + timeout := time.After(100 * time.Millisecond) + for { + if jsonCacheLoaded && protoCacheLoaded { + // test passed: both cache eager-loaded + return + } + select { + case <-timeout: + t.Errorf("timeout waiting for cache to eager load") + break + default: + time.Sleep(time.Millisecond) + } + } +} diff --git a/pkg/handler3/handler.go b/pkg/handler3/handler.go index 6fa03b278..c225cc6dd 100644 --- a/pkg/handler3/handler.go +++ b/pkg/handler3/handler.go @@ -21,6 +21,7 @@ import ( "crypto/sha512" "encoding/json" "fmt" + "math" "mime" "net/http" "sort" @@ -51,7 +52,6 @@ const ( // OpenAPIService is the service responsible for serving OpenAPI spec. It has // the ability to safely change the spec while serving it. -// OpenAPI V3 currently does not use the lazy marshaling strategy that OpenAPI V2 is using type OpenAPIService struct { // rwMutex protects All members of this service. rwMutex sync.RWMutex @@ -62,6 +62,10 @@ type OpenAPIService struct { type OpenAPIV3Group struct { rwMutex sync.RWMutex + // eagerMarshalingTimer fires if no updates to the spec are made + // for the defined duration. + eagerMarshalingTimer *time.Timer + lastModified time.Time pbCache handler.HandlerCache @@ -85,6 +89,18 @@ func NewOpenAPIService(spec *spec.Swagger) (*OpenAPIService, error) { return o, nil } +// NewOpenAPIGroup builds an empty OpenAPIGroup +func NewOpenAPIGroup() *OpenAPIV3Group { + g := &OpenAPIV3Group{} + g.eagerMarshalingTimer = time.AfterFunc( + time.Duration(math.MaxInt64), // placeholder duration, will be reset later + func() { + _, _, _ = g.jsonCache.Get() + _, _, _ = g.pbCache.Get() + }) + return g +} + func (o *OpenAPIService) getGroupBytes() ([]byte, error) { o.rwMutex.RLock() defer o.rwMutex.RUnlock() @@ -133,7 +149,7 @@ func (o *OpenAPIService) UpdateGroupVersion(group string, openapi *spec3.OpenAPI } if _, ok := o.v3Schema[group]; !ok { - o.v3Schema[group] = &OpenAPIV3Group{} + o.v3Schema[group] = NewOpenAPIGroup() } return o.v3Schema[group].UpdateSpec(specBytes) } @@ -211,6 +227,8 @@ func (o *OpenAPIV3Group) UpdateSpec(specBytes []byte) (err error) { o.rwMutex.Lock() defer o.rwMutex.Unlock() + _ = o.eagerMarshalingTimer.Reset(handler.EagerMarshalingCoolDown) + o.pbCache = o.pbCache.New(func() ([]byte, error) { return ToV3ProtoBinary(specBytes) }) diff --git a/pkg/internal/handler/eager.go b/pkg/internal/handler/eager.go new file mode 100644 index 000000000..ae8a8fbef --- /dev/null +++ b/pkg/internal/handler/eager.go @@ -0,0 +1,23 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package handler + +import "time" + +// EagerMarshalingCoolDown is the duration after the last update to the spec +// before an eager marshalling of the spec happens. +var EagerMarshalingCoolDown = 20 * time.Second