From 65f13f3563ce1a72ec76d3d477eb8323993cfe94 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sun, 9 Aug 2026 02:59:41 +0200 Subject: [PATCH 1/2] refact: recycle objects through swag/pools The pools here did what swag/pools does generically, in 1384 lines across a release file and a debug one selected by build tag. They are now fifteen declarations of pools.Pool[T], and the hand-written instrumentation gives way to the shared one: go test -tags poolsdebug ./... That build tracks every borrow and redeem, panics on a double redeem, a foreign redeem or a borrow of an object still checked out, and reports what was borrowed and never given back. The suite passes under it, parallel validation included. Result implements pools.Resettable, so it is cleared when borrowed as it was before, and now when redeemed as well: a result idling in the pool no longer keeps a reference to the document it validated. Results are borrowed straight from the pool rather than through a helper, so that a leak is attributed to the code that borrowed it rather than to the helper. Redeeming still goes through one, since the shared empty result was never borrowed and must not be handed over. The leak assertion, previously reachable only under the local build tag, now runs in both modes and covers validating a specification, validating data against a schema, and the nil-schema path that returns that empty result. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Frederic BIDON --- .github/workflows/go-test.yml | 2 + default_validator.go | 20 +- example_validator.go | 20 +- formats.go | 6 +- go.mod | 2 +- helpers.go | 2 +- object_validator.go | 14 +- pools.go | 420 +++----------- pools_debug.go | 1015 --------------------------------- pools_debug_test.go | 31 - pools_leak_test.go | 81 +++ result.go | 20 +- result_location_test.go | 2 +- schema.go | 8 +- schema_props.go | 30 +- slice_validator.go | 6 +- spec.go | 30 +- spec_ref_warnings.go | 2 +- type.go | 4 +- validator.go | 44 +- validator_test.go | 2 +- 21 files changed, 262 insertions(+), 1499 deletions(-) delete mode 100644 pools_debug.go delete mode 100644 pools_debug_test.go create mode 100644 pools_leak_test.go diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index ec67258..1722ebc 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -14,4 +14,6 @@ on: jobs: test: uses: go-openapi/ci-workflows/.github/workflows/go-test.yml@befbc52f24215555cb601b0d824b2987c1d49d0b # v0.4.3 + with: + extra-flags: '-tags poolsdebug' secrets: inherit diff --git a/default_validator.go b/default_validator.go index d6da321..010a10f 100644 --- a/default_validator.go +++ b/default_validator.go @@ -17,7 +17,7 @@ type defaultValidator struct { // Validate validates the default values declared in the swagger spec. func (d *defaultValidator) Validate() *Result { - errs := pools.poolOfResults.BorrowResult() // will redeem when merged + errs := validatorPools.results.Borrow() // will redeem when merged if d == nil || d.SpecValidator == nil { return errs @@ -73,7 +73,7 @@ func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result { // every default value that is specified must validate against the schema for that property // headers, items, parameters, schema - res := pools.poolOfResults.BorrowResult() // will redeem when merged + res := validatorPools.results.Borrow() // will redeem when merged s := d.SpecValidator for method, pathItem := range s.expandedAnalyzer().Operations() { @@ -96,7 +96,7 @@ func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result { res.addErrorsAt(s.parameterPath(path, method, param.In, param.Name), defaultValueDoesNotValidateMsg(param.Name, param.In)) res.Merge(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } @@ -107,7 +107,7 @@ func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result { res.addErrorsAt(s.parameterPath(path, method, param.In, param.Name), defaultValueItemsDoesNotValidateMsg(param.Name, param.In)) res.Merge(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } @@ -118,7 +118,7 @@ func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result { res.addErrorsAt(s.parameterPath(path, method, param.In, param.Name), defaultValueDoesNotValidateMsg(param.Name, param.In)) res.Merge(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } } @@ -173,7 +173,7 @@ func (d *defaultValidator) validateDefaultInResponse( res.addErrorsAt(responseHeaderPath(path, method, responseCodeAsStr, nm), defaultValueHeaderDoesNotValidateMsg(operationID, nm, responseName)) res.Merge(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } @@ -184,7 +184,7 @@ func (d *defaultValidator) validateDefaultInResponse( res.addErrorsAt(responseHeaderPath(path, method, responseCodeAsStr, nm), defaultValueHeaderItemsDoesNotValidateMsg(operationID, nm, responseName)) res.Merge(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } @@ -206,7 +206,7 @@ func (d *defaultValidator) validateDefaultInResponse( res.addErrorsAt(responsePath(path, method, responseCodeAsStr), defaultValueInDoesNotValidateMsg(operationID, responseName)) res.Merge(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } return res @@ -218,7 +218,7 @@ func (d *defaultValidator) validateDefaultValueSchemaAgainstSchema(path pathSegm return nil } d.beingVisited(path) - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() s := d.SpecValidator if schema.Default != nil { @@ -264,7 +264,7 @@ func (d *defaultValidator) validateDefaultValueSchemaAgainstSchema(path pathSegm // NOTE: Temporary duplicated code. Need to refactor with examples func (d *defaultValidator) validateDefaultValueItemsAgainstSchema(path pathSegments, in string, root any, items *spec.Items) *Result { - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() s := d.SpecValidator if items != nil { if items.Default != nil { diff --git a/example_validator.go b/example_validator.go index 07272a7..1f830a6 100644 --- a/example_validator.go +++ b/example_validator.go @@ -22,7 +22,7 @@ type exampleValidator struct { // - individual property // - responses func (ex *exampleValidator) Validate() *Result { - errs := pools.poolOfResults.BorrowResult() + errs := validatorPools.results.Borrow() if ex == nil || ex.SpecValidator == nil { return errs @@ -63,7 +63,7 @@ func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result { // in: schemas, properties, object, items // not in: headers, parameters without schema - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() s := ex.SpecValidator for method, pathItem := range s.expandedAnalyzer().Operations() { @@ -86,7 +86,7 @@ func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result { res.addWarningsAt(s.parameterPath(path, method, param.In, param.Name), exampleValueDoesNotValidateMsg(param.Name, param.In)) res.MergeAsWarnings(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } @@ -97,7 +97,7 @@ func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result { res.addWarningsAt(s.parameterPath(path, method, param.In, param.Name), exampleValueItemsDoesNotValidateMsg(param.Name, param.In)) res.Merge(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } @@ -108,7 +108,7 @@ func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result { res.addWarningsAt(s.parameterPath(path, method, param.In, param.Name), exampleValueDoesNotValidateMsg(param.Name, param.In)) res.Merge(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } } @@ -163,7 +163,7 @@ func (ex *exampleValidator) validateExampleInResponse( res.addWarningsAt(responseHeaderPath(path, method, responseCodeAsStr, nm), exampleValueHeaderDoesNotValidateMsg(operationID, nm, responseName)) res.MergeAsWarnings(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } @@ -174,7 +174,7 @@ func (ex *exampleValidator) validateExampleInResponse( res.addWarningsAt(responseHeaderPath(path, method, responseCodeAsStr, nm), exampleValueHeaderItemsDoesNotValidateMsg(operationID, nm, responseName)) res.MergeAsWarnings(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } @@ -196,7 +196,7 @@ func (ex *exampleValidator) validateExampleInResponse( res.addWarningsAt(responsePath(path, method, responseCodeAsStr), exampleValueInDoesNotValidateMsg(operationID, responseName)) res.Merge(red) } else if red.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(red) + redeemResult(red) } } @@ -228,7 +228,7 @@ func (ex *exampleValidator) validateExampleValueSchemaAgainstSchema(path pathSeg } ex.beingVisited(path) s := ex.SpecValidator - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() if schema.Example != nil { res.MergeAsWarnings( @@ -274,7 +274,7 @@ func (ex *exampleValidator) validateExampleValueSchemaAgainstSchema(path pathSeg // func (ex *exampleValidator) validateExampleValueItemsAgainstSchema(path pathSegments, in string, root any, items *spec.Items) *Result { - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() s := ex.SpecValidator if items != nil { if items.Example != nil { diff --git a/formats.go b/formats.go index a050b39..bb58f79 100644 --- a/formats.go +++ b/formats.go @@ -25,7 +25,7 @@ func newFormatValidator(path pathSegments, in, format string, formats strfmt.Reg var f *formatValidator if opts.recycleValidators { - f = pools.poolOfFormatValidators.BorrowValidator() + f = validatorPools.formatValidators.Borrow() } else { f = new(formatValidator) } @@ -67,7 +67,7 @@ func (f *formatValidator) Validate(val any) *Result { var result *Result if f.Options.recycleResult { - result = pools.poolOfResults.BorrowResult() + result = validatorPools.results.Borrow() } else { result = new(Result) } @@ -89,5 +89,5 @@ func (f *formatValidator) setPath(path pathSegments) { } func (f *formatValidator) redeem() { - pools.poolOfFormatValidators.RedeemValidator(f) + validatorPools.formatValidators.Redeem(f) } diff --git a/go.mod b/go.mod index 230ce38..6002895 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/go-openapi/swag/fileutils v0.28.0 github.com/go-openapi/swag/jsonutils v0.28.0 github.com/go-openapi/swag/loading v0.28.0 + github.com/go-openapi/swag/pools v0.28.0 github.com/go-openapi/swag/stringutils v0.28.0 github.com/go-openapi/testify/v2 v2.6.0 go.yaml.in/yaml/v3 v3.0.5 @@ -19,7 +20,6 @@ require ( require ( github.com/go-openapi/jsonreference v1.0.0 // indirect github.com/go-openapi/swag/mangling v0.28.0 // indirect - github.com/go-openapi/swag/pools v0.28.0 // indirect github.com/go-openapi/swag/typeutils v0.28.0 // indirect github.com/go-openapi/swag/yamlutils v0.28.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect diff --git a/helpers.go b/helpers.go index 027f134..b8f331e 100644 --- a/helpers.go +++ b/helpers.go @@ -160,7 +160,7 @@ func (h *errorHelper) sErr(err errors.Error, recycle bool) *Result { func (h *errorHelper) sErrAt(at pathSegments, err errors.Error, recycle bool) *Result { var result *Result if recycle { - result = pools.poolOfResults.BorrowResult() + result = validatorPools.results.Borrow() } else { result = new(Result) } diff --git a/object_validator.go b/object_validator.go index 2cc8dce..2d14557 100644 --- a/object_validator.go +++ b/object_validator.go @@ -37,7 +37,7 @@ func newObjectValidator(path pathSegments, in string, var v *objectValidator if opts.recycleValidators { - v = pools.poolOfObjectValidators.BorrowValidator() + v = validatorPools.objectValidators.Borrow() } else { v = new(objectValidator) } @@ -83,7 +83,7 @@ func (o *objectValidator) Validate(data any) *Result { var res *Result if o.Options.recycleResult { - res = pools.poolOfResults.BorrowResult() + res = validatorPools.results.Borrow() } else { res = new(Result) } @@ -328,9 +328,9 @@ func (o *objectValidator) validatePropertiesSchema(val map[string]any, res *Resu // Property types: // - regular Property - pSchema := pools.poolOfSchemas.BorrowSchema() // recycle a spec.Schema object which lifespan extends only to the validation of properties + pSchema := validatorPools.schemas.Borrow() // recycle a spec.Schema object which lifespan extends only to the validation of properties defer func() { - pools.poolOfSchemas.RedeemSchema(pSchema) + validatorPools.schemas.Redeem(pSchema) }() for pName := range o.Properties { @@ -387,9 +387,9 @@ func (o *objectValidator) validatePatternProperty(key string, value any, result succeededOnce := false patterns := make([]string, 0, len(o.PatternProperties)) - schema := pools.poolOfSchemas.BorrowSchema() + schema := validatorPools.schemas.Borrow() defer func() { - pools.poolOfSchemas.RedeemSchema(schema) + validatorPools.schemas.Redeem(schema) }() for k := range o.PatternProperties { @@ -420,5 +420,5 @@ func (o *objectValidator) setPath(path pathSegments) { } func (o *objectValidator) redeem() { - pools.poolOfObjectValidators.RedeemValidator(o) + validatorPools.objectValidators.Redeem(o) } diff --git a/pools.go b/pools.go index c8936bd..0bc95d1 100644 --- a/pools.go +++ b/pools.go @@ -1,369 +1,87 @@ // SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -//go:build !validatedebug - package validate import ( - "sync" - "github.com/go-openapi/spec" + "github.com/go-openapi/swag/pools" ) -var pools allPools +// validatorPools recycles the objects allocated while validating. +// +// Validation allocates a validator per schema node and a result per check, so +// the same handful of types are built and thrown away constantly. Recycling +// them is what keeps validating a large specification affordable. +// +// Build with the "poolsdebug" tag to have every borrow and redeem tracked: +// misuse then panics where it happens rather than corrupting a pool, and +// [pools.AssertNoLeaks] reports what was borrowed and never given back. +var validatorPools allPools func init() { resetPools() } +// resetPools builds a fresh set of pools. +// +// Recycling an object twice leaves a pool holding it twice, and it would then +// be handed to two borrowers at once. A test that provokes such misuse has to +// start the next one from clean pools. func resetPools() { - // NOTE: for testing purpose, we might want to reset pools after calling Validate twice. - // The pool is corrupted in that case: calling Put twice inserts a duplicate in the pool - // and further calls to Get are mishandled. - - pools = allPools{ - poolOfSchemaValidators: schemaValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &SchemaValidator{} - - return s - }, - }, - }, - poolOfObjectValidators: objectValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &objectValidator{} - - return s - }, - }, - }, - poolOfSliceValidators: sliceValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &schemaSliceValidator{} - - return s - }, - }, - }, - poolOfItemsValidators: itemsValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &itemsValidator{} - - return s - }, - }, - }, - poolOfBasicCommonValidators: basicCommonValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &basicCommonValidator{} - - return s - }, - }, - }, - poolOfHeaderValidators: headerValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &HeaderValidator{} - - return s - }, - }, - }, - poolOfParamValidators: paramValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &ParamValidator{} - - return s - }, - }, - }, - poolOfBasicSliceValidators: basicSliceValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &basicSliceValidator{} - - return s - }, - }, - }, - poolOfNumberValidators: numberValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &numberValidator{} - - return s - }, - }, - }, - poolOfStringValidators: stringValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &stringValidator{} - - return s - }, - }, - }, - poolOfSchemaPropsValidators: schemaPropsValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &schemaPropsValidator{} - - return s - }, - }, - }, - poolOfFormatValidators: formatValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &formatValidator{} - - return s - }, - }, - }, - poolOfTypeValidators: typeValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &typeValidator{} - - return s - }, - }, - }, - poolOfSchemas: schemasPool{ - Pool: &sync.Pool{ - New: func() any { - s := &spec.Schema{} - - return s - }, - }, - }, - poolOfResults: resultsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &Result{} - - return s - }, - }, - }, - } -} - -type ( - allPools struct { - // memory pools for all validator objects. - // - // Each pool can be borrowed from and redeemed to. - poolOfSchemaValidators schemaValidatorsPool - poolOfObjectValidators objectValidatorsPool - poolOfSliceValidators sliceValidatorsPool - poolOfItemsValidators itemsValidatorsPool - poolOfBasicCommonValidators basicCommonValidatorsPool - poolOfHeaderValidators headerValidatorsPool - poolOfParamValidators paramValidatorsPool - poolOfBasicSliceValidators basicSliceValidatorsPool - poolOfNumberValidators numberValidatorsPool - poolOfStringValidators stringValidatorsPool - poolOfSchemaPropsValidators schemaPropsValidatorsPool - poolOfFormatValidators formatValidatorsPool - poolOfTypeValidators typeValidatorsPool - poolOfSchemas schemasPool - poolOfResults resultsPool - } - - schemaValidatorsPool struct { - *sync.Pool - } - - objectValidatorsPool struct { - *sync.Pool - } - - sliceValidatorsPool struct { - *sync.Pool - } - - itemsValidatorsPool struct { - *sync.Pool - } - - basicCommonValidatorsPool struct { - *sync.Pool - } - - headerValidatorsPool struct { - *sync.Pool - } - - paramValidatorsPool struct { - *sync.Pool - } - - basicSliceValidatorsPool struct { - *sync.Pool - } - - numberValidatorsPool struct { - *sync.Pool - } - - stringValidatorsPool struct { - *sync.Pool - } - - schemaPropsValidatorsPool struct { - *sync.Pool - } - - formatValidatorsPool struct { - *sync.Pool - } - - typeValidatorsPool struct { - *sync.Pool - } - - schemasPool struct { - *sync.Pool - } - - resultsPool struct { - *sync.Pool - } -) - -func (p schemaValidatorsPool) BorrowValidator() *SchemaValidator { - return p.Get().(*SchemaValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p schemaValidatorsPool) RedeemValidator(s *SchemaValidator) { - // NOTE: s might be nil. In that case, Put is a noop. - p.Put(s) -} - -func (p objectValidatorsPool) BorrowValidator() *objectValidator { - return p.Get().(*objectValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p objectValidatorsPool) RedeemValidator(s *objectValidator) { - p.Put(s) -} - -func (p sliceValidatorsPool) BorrowValidator() *schemaSliceValidator { - return p.Get().(*schemaSliceValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p sliceValidatorsPool) RedeemValidator(s *schemaSliceValidator) { - p.Put(s) -} - -func (p itemsValidatorsPool) BorrowValidator() *itemsValidator { - return p.Get().(*itemsValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p itemsValidatorsPool) RedeemValidator(s *itemsValidator) { - p.Put(s) -} - -func (p basicCommonValidatorsPool) BorrowValidator() *basicCommonValidator { - return p.Get().(*basicCommonValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p basicCommonValidatorsPool) RedeemValidator(s *basicCommonValidator) { - p.Put(s) -} - -func (p headerValidatorsPool) BorrowValidator() *HeaderValidator { - return p.Get().(*HeaderValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p headerValidatorsPool) RedeemValidator(s *HeaderValidator) { - p.Put(s) -} - -func (p paramValidatorsPool) BorrowValidator() *ParamValidator { - return p.Get().(*ParamValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p paramValidatorsPool) RedeemValidator(s *ParamValidator) { - p.Put(s) -} - -func (p basicSliceValidatorsPool) BorrowValidator() *basicSliceValidator { - return p.Get().(*basicSliceValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p basicSliceValidatorsPool) RedeemValidator(s *basicSliceValidator) { - p.Put(s) -} - -func (p numberValidatorsPool) BorrowValidator() *numberValidator { - return p.Get().(*numberValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p numberValidatorsPool) RedeemValidator(s *numberValidator) { - p.Put(s) -} - -func (p stringValidatorsPool) BorrowValidator() *stringValidator { - return p.Get().(*stringValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p stringValidatorsPool) RedeemValidator(s *stringValidator) { - p.Put(s) -} - -func (p schemaPropsValidatorsPool) BorrowValidator() *schemaPropsValidator { - return p.Get().(*schemaPropsValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p schemaPropsValidatorsPool) RedeemValidator(s *schemaPropsValidator) { - p.Put(s) -} - -func (p formatValidatorsPool) BorrowValidator() *formatValidator { - return p.Get().(*formatValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p formatValidatorsPool) RedeemValidator(s *formatValidator) { - p.Put(s) -} - -func (p typeValidatorsPool) BorrowValidator() *typeValidator { - return p.Get().(*typeValidator) //nolint:forcetypeassert // pool New always returns this type -} - -func (p typeValidatorsPool) RedeemValidator(s *typeValidator) { - p.Put(s) -} - -func (p schemasPool) BorrowSchema() *spec.Schema { - return p.Get().(*spec.Schema) //nolint:forcetypeassert // pool New always returns this type -} - -func (p schemasPool) RedeemSchema(s *spec.Schema) { - p.Put(s) -} - -func (p resultsPool) BorrowResult() *Result { - return p.Get().(*Result).cleared() //nolint:forcetypeassert // pool New always returns *Result -} - -func (p resultsPool) RedeemResult(s *Result) { - if s == emptyResult { + validatorPools = allPools{ + schemaValidators: pools.New[SchemaValidator](), + objectValidators: pools.New[objectValidator](), + sliceValidators: pools.New[schemaSliceValidator](), + itemsValidators: pools.New[itemsValidator](), + basicCommonValidators: pools.New[basicCommonValidator](), + headerValidators: pools.New[HeaderValidator](), + paramValidators: pools.New[ParamValidator](), + basicSliceValidators: pools.New[basicSliceValidator](), + numberValidators: pools.New[numberValidator](), + stringValidators: pools.New[stringValidator](), + schemaPropsValidators: pools.New[schemaPropsValidator](), + formatValidators: pools.New[formatValidator](), + typeValidators: pools.New[typeValidator](), + schemas: pools.New[spec.Schema](), + results: pools.New[Result](), + } +} + +// allPools is the set of pools shared by the validators of this package. +type allPools struct { + schemaValidators *pools.Pool[SchemaValidator] + objectValidators *pools.Pool[objectValidator] + sliceValidators *pools.Pool[schemaSliceValidator] + itemsValidators *pools.Pool[itemsValidator] + basicCommonValidators *pools.Pool[basicCommonValidator] + headerValidators *pools.Pool[HeaderValidator] + paramValidators *pools.Pool[ParamValidator] + basicSliceValidators *pools.Pool[basicSliceValidator] + numberValidators *pools.Pool[numberValidator] + stringValidators *pools.Pool[stringValidator] + schemaPropsValidators *pools.Pool[schemaPropsValidator] + formatValidators *pools.Pool[formatValidator] + typeValidators *pools.Pool[typeValidator] + schemas *pools.Pool[spec.Schema] + results *pools.Pool[Result] +} + +// redeemResult returns a result to the pool. +// +// emptyResult is a shared value that was never borrowed, so it is not the +// pool's to take back: handing it over would be reported as a foreign redeem, +// rightly. +// +// Results are borrowed straight from the pool rather than through a helper, +// so that the instrumented build attributes a leak to the code that borrowed it. +// +// This wrapper costs that attribution on redeem, where a double redeem still +// names the offending call site in the panic it raises. +func redeemResult(r *Result) { + if r == emptyResult { return } - p.Put(s) + + validatorPools.results.Redeem(r) } diff --git a/pools_debug.go b/pools_debug.go deleted file mode 100644 index d123ed4..0000000 --- a/pools_debug.go +++ /dev/null @@ -1,1015 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers -// SPDX-License-Identifier: Apache-2.0 - -//go:build validatedebug - -package validate - -import ( - "fmt" - "runtime" - "sync" - "testing" - - "github.com/go-openapi/spec" -) - -// This version of the pools is to be used for debugging and testing, with build tag "validatedebug". -// -// In this mode, the pools are tracked for allocation and redemption of borrowed objects, so we can -// verify a few behaviors of the validators. The debug pools panic when an invalid usage pattern is detected. - -var pools allPools - -func init() { - resetPools() -} - -func resetPools() { - // NOTE: for testing purpose, we might want to reset pools after calling Validate twice. - // The pool is corrupted in that case: calling Put twice inserts a duplicate in the pool - // and further calls to Get are mishandled. - - pools = allPools{ - poolOfSchemaValidators: schemaValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &SchemaValidator{} - - return s - }, - }, - debugMap: make(map[*SchemaValidator]status), - allocMap: make(map[*SchemaValidator]string), - redeemMap: make(map[*SchemaValidator]string), - }, - poolOfObjectValidators: objectValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &objectValidator{} - - return s - }, - }, - debugMap: make(map[*objectValidator]status), - allocMap: make(map[*objectValidator]string), - redeemMap: make(map[*objectValidator]string), - }, - poolOfSliceValidators: sliceValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &schemaSliceValidator{} - - return s - }, - }, - debugMap: make(map[*schemaSliceValidator]status), - allocMap: make(map[*schemaSliceValidator]string), - redeemMap: make(map[*schemaSliceValidator]string), - }, - poolOfItemsValidators: itemsValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &itemsValidator{} - - return s - }, - }, - debugMap: make(map[*itemsValidator]status), - allocMap: make(map[*itemsValidator]string), - redeemMap: make(map[*itemsValidator]string), - }, - poolOfBasicCommonValidators: basicCommonValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &basicCommonValidator{} - - return s - }, - }, - debugMap: make(map[*basicCommonValidator]status), - allocMap: make(map[*basicCommonValidator]string), - redeemMap: make(map[*basicCommonValidator]string), - }, - poolOfHeaderValidators: headerValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &HeaderValidator{} - - return s - }, - }, - debugMap: make(map[*HeaderValidator]status), - allocMap: make(map[*HeaderValidator]string), - redeemMap: make(map[*HeaderValidator]string), - }, - poolOfParamValidators: paramValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &ParamValidator{} - - return s - }, - }, - debugMap: make(map[*ParamValidator]status), - allocMap: make(map[*ParamValidator]string), - redeemMap: make(map[*ParamValidator]string), - }, - poolOfBasicSliceValidators: basicSliceValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &basicSliceValidator{} - - return s - }, - }, - debugMap: make(map[*basicSliceValidator]status), - allocMap: make(map[*basicSliceValidator]string), - redeemMap: make(map[*basicSliceValidator]string), - }, - poolOfNumberValidators: numberValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &numberValidator{} - - return s - }, - }, - debugMap: make(map[*numberValidator]status), - allocMap: make(map[*numberValidator]string), - redeemMap: make(map[*numberValidator]string), - }, - poolOfStringValidators: stringValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &stringValidator{} - - return s - }, - }, - debugMap: make(map[*stringValidator]status), - allocMap: make(map[*stringValidator]string), - redeemMap: make(map[*stringValidator]string), - }, - poolOfSchemaPropsValidators: schemaPropsValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &schemaPropsValidator{} - - return s - }, - }, - debugMap: make(map[*schemaPropsValidator]status), - allocMap: make(map[*schemaPropsValidator]string), - redeemMap: make(map[*schemaPropsValidator]string), - }, - poolOfFormatValidators: formatValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &formatValidator{} - - return s - }, - }, - debugMap: make(map[*formatValidator]status), - allocMap: make(map[*formatValidator]string), - redeemMap: make(map[*formatValidator]string), - }, - poolOfTypeValidators: typeValidatorsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &typeValidator{} - - return s - }, - }, - debugMap: make(map[*typeValidator]status), - allocMap: make(map[*typeValidator]string), - redeemMap: make(map[*typeValidator]string), - }, - poolOfSchemas: schemasPool{ - Pool: &sync.Pool{ - New: func() any { - s := &spec.Schema{} - - return s - }, - }, - debugMap: make(map[*spec.Schema]status), - allocMap: make(map[*spec.Schema]string), - redeemMap: make(map[*spec.Schema]string), - }, - poolOfResults: resultsPool{ - Pool: &sync.Pool{ - New: func() any { - s := &Result{} - - return s - }, - }, - debugMap: make(map[*Result]status), - allocMap: make(map[*Result]string), - redeemMap: make(map[*Result]string), - }, - } -} - -const ( - statusFresh status = iota + 1 - statusRecycled - statusRedeemed -) - -func (s status) String() string { - switch s { - case statusFresh: - return "fresh" - case statusRecycled: - return "recycled" - case statusRedeemed: - return "redeemed" - default: - panic(fmt.Errorf("invalid status: %d", s)) - } -} - -type ( - // Debug - status uint8 - - allPools struct { - // memory pools for all validator objects. - // - // Each pool can be borrowed from and redeemed to. - poolOfSchemaValidators schemaValidatorsPool - poolOfObjectValidators objectValidatorsPool - poolOfSliceValidators sliceValidatorsPool - poolOfItemsValidators itemsValidatorsPool - poolOfBasicCommonValidators basicCommonValidatorsPool - poolOfHeaderValidators headerValidatorsPool - poolOfParamValidators paramValidatorsPool - poolOfBasicSliceValidators basicSliceValidatorsPool - poolOfNumberValidators numberValidatorsPool - poolOfStringValidators stringValidatorsPool - poolOfSchemaPropsValidators schemaPropsValidatorsPool - poolOfFormatValidators formatValidatorsPool - poolOfTypeValidators typeValidatorsPool - poolOfSchemas schemasPool - poolOfResults resultsPool - } - - schemaValidatorsPool struct { - *sync.Pool - debugMap map[*SchemaValidator]status - allocMap map[*SchemaValidator]string - redeemMap map[*SchemaValidator]string - mx sync.Mutex - } - - objectValidatorsPool struct { - *sync.Pool - debugMap map[*objectValidator]status - allocMap map[*objectValidator]string - redeemMap map[*objectValidator]string - mx sync.Mutex - } - - sliceValidatorsPool struct { - *sync.Pool - debugMap map[*schemaSliceValidator]status - allocMap map[*schemaSliceValidator]string - redeemMap map[*schemaSliceValidator]string - mx sync.Mutex - } - - itemsValidatorsPool struct { - *sync.Pool - debugMap map[*itemsValidator]status - allocMap map[*itemsValidator]string - redeemMap map[*itemsValidator]string - mx sync.Mutex - } - - basicCommonValidatorsPool struct { - *sync.Pool - debugMap map[*basicCommonValidator]status - allocMap map[*basicCommonValidator]string - redeemMap map[*basicCommonValidator]string - mx sync.Mutex - } - - headerValidatorsPool struct { - *sync.Pool - debugMap map[*HeaderValidator]status - allocMap map[*HeaderValidator]string - redeemMap map[*HeaderValidator]string - mx sync.Mutex - } - - paramValidatorsPool struct { - *sync.Pool - debugMap map[*ParamValidator]status - allocMap map[*ParamValidator]string - redeemMap map[*ParamValidator]string - mx sync.Mutex - } - - basicSliceValidatorsPool struct { - *sync.Pool - debugMap map[*basicSliceValidator]status - allocMap map[*basicSliceValidator]string - redeemMap map[*basicSliceValidator]string - mx sync.Mutex - } - - numberValidatorsPool struct { - *sync.Pool - debugMap map[*numberValidator]status - allocMap map[*numberValidator]string - redeemMap map[*numberValidator]string - mx sync.Mutex - } - - stringValidatorsPool struct { - *sync.Pool - debugMap map[*stringValidator]status - allocMap map[*stringValidator]string - redeemMap map[*stringValidator]string - mx sync.Mutex - } - - schemaPropsValidatorsPool struct { - *sync.Pool - debugMap map[*schemaPropsValidator]status - allocMap map[*schemaPropsValidator]string - redeemMap map[*schemaPropsValidator]string - mx sync.Mutex - } - - formatValidatorsPool struct { - *sync.Pool - debugMap map[*formatValidator]status - allocMap map[*formatValidator]string - redeemMap map[*formatValidator]string - mx sync.Mutex - } - - typeValidatorsPool struct { - *sync.Pool - debugMap map[*typeValidator]status - allocMap map[*typeValidator]string - redeemMap map[*typeValidator]string - mx sync.Mutex - } - - schemasPool struct { - *sync.Pool - debugMap map[*spec.Schema]status - allocMap map[*spec.Schema]string - redeemMap map[*spec.Schema]string - mx sync.Mutex - } - - resultsPool struct { - *sync.Pool - debugMap map[*Result]status - allocMap map[*Result]string - redeemMap map[*Result]string - mx sync.Mutex - } -) - -func (p *schemaValidatorsPool) BorrowValidator() *SchemaValidator { - s := p.Get().(*SchemaValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled schema should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *schemaValidatorsPool) RedeemValidator(s *SchemaValidator) { - // NOTE: s might be nil. In that case, Put is a noop. - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed schema should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed schema should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *objectValidatorsPool) BorrowValidator() *objectValidator { - s := p.Get().(*objectValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled object should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *objectValidatorsPool) RedeemValidator(s *objectValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed object should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed object should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *sliceValidatorsPool) BorrowValidator() *schemaSliceValidator { - s := p.Get().(*schemaSliceValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled schemaSliceValidator should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *sliceValidatorsPool) RedeemValidator(s *schemaSliceValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed schemaSliceValidator should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed schemaSliceValidator should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *itemsValidatorsPool) BorrowValidator() *itemsValidator { - s := p.Get().(*itemsValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled itemsValidator should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *itemsValidatorsPool) RedeemValidator(s *itemsValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed itemsValidator should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed itemsValidator should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *basicCommonValidatorsPool) BorrowValidator() *basicCommonValidator { - s := p.Get().(*basicCommonValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled basicCommonValidator should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *basicCommonValidatorsPool) RedeemValidator(s *basicCommonValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed basicCommonValidator should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed basicCommonValidator should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *headerValidatorsPool) BorrowValidator() *HeaderValidator { - s := p.Get().(*HeaderValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled HeaderValidator should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *headerValidatorsPool) RedeemValidator(s *HeaderValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed header should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed header should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *paramValidatorsPool) BorrowValidator() *ParamValidator { - s := p.Get().(*ParamValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled param should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *paramValidatorsPool) RedeemValidator(s *ParamValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed param should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed param should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *basicSliceValidatorsPool) BorrowValidator() *basicSliceValidator { - s := p.Get().(*basicSliceValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled basicSliceValidator should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *basicSliceValidatorsPool) RedeemValidator(s *basicSliceValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed basicSliceValidator should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed basicSliceValidator should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *numberValidatorsPool) BorrowValidator() *numberValidator { - s := p.Get().(*numberValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled number should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *numberValidatorsPool) RedeemValidator(s *numberValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed number should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed number should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *stringValidatorsPool) BorrowValidator() *stringValidator { - s := p.Get().(*stringValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled string should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *stringValidatorsPool) RedeemValidator(s *stringValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed string should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed string should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *schemaPropsValidatorsPool) BorrowValidator() *schemaPropsValidator { - s := p.Get().(*schemaPropsValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled param should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *schemaPropsValidatorsPool) RedeemValidator(s *schemaPropsValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed schemaProps should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed schemaProps should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *formatValidatorsPool) BorrowValidator() *formatValidator { - s := p.Get().(*formatValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled format should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *formatValidatorsPool) RedeemValidator(s *formatValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed format should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed format should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *typeValidatorsPool) BorrowValidator() *typeValidator { - s := p.Get().(*typeValidator) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled type should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *typeValidatorsPool) RedeemValidator(s *typeValidator) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed type should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic(fmt.Errorf("redeemed type should have been allocated from a fresh or recycled pointer. Got status %s, already redeamed at: %s", x, p.redeemMap[s])) - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *schemasPool) BorrowSchema() *spec.Schema { - s := p.Get().(*spec.Schema) - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled spec.Schema should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *schemasPool) RedeemSchema(s *spec.Schema) { - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed spec.Schema should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed spec.Schema should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *resultsPool) BorrowResult() *Result { - s := p.Get().(*Result).cleared() - - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - p.debugMap[s] = statusFresh - } else { - if x != statusRedeemed { - panic("recycled result should have been redeemed") - } - p.debugMap[s] = statusRecycled - } - p.allocMap[s] = caller() - - return s -} - -func (p *resultsPool) RedeemResult(s *Result) { - if s == emptyResult { - if len(s.Errors) > 0 || len(s.Warnings) > 0 { - panic("empty result should not mutate") - } - return - } - p.mx.Lock() - defer p.mx.Unlock() - x, ok := p.debugMap[s] - if !ok { - panic("redeemed Result should have been allocated") - } - if x != statusRecycled && x != statusFresh { - panic("redeemed Result should have been allocated from a fresh or recycled pointer") - } - p.debugMap[s] = statusRedeemed - p.redeemMap[s] = caller() - p.Put(s) -} - -func (p *allPools) allIsRedeemed(t testing.TB) bool { - outcome := true - for k, v := range p.poolOfSchemaValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("schemaValidator should be redeemed. Allocated by: %s", p.poolOfSchemaValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfObjectValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("objectValidator should be redeemed. Allocated by: %s", p.poolOfObjectValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfSliceValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("sliceValidator should be redeemed. Allocated by: %s", p.poolOfSliceValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfItemsValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("itemsValidator should be redeemed. Allocated by: %s", p.poolOfItemsValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfBasicCommonValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("basicCommonValidator should be redeemed. Allocated by: %s", p.poolOfBasicCommonValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfHeaderValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("headerValidator should be redeemed. Allocated by: %s", p.poolOfHeaderValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfParamValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("paramValidator should be redeemed. Allocated by: %s", p.poolOfParamValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfBasicSliceValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("basicSliceValidator should be redeemed. Allocated by: %s", p.poolOfBasicSliceValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfNumberValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("numberValidator should be redeemed. Allocated by: %s", p.poolOfNumberValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfStringValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("stringValidator should be redeemed. Allocated by: %s", p.poolOfStringValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfSchemaPropsValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("schemaPropsValidator should be redeemed. Allocated by: %s", p.poolOfSchemaPropsValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfFormatValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("formatValidator should be redeemed. Allocated by: %s", p.poolOfFormatValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfTypeValidators.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("typeValidator should be redeemed. Allocated by: %s", p.poolOfTypeValidators.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfSchemas.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("schemas should be redeemed. Allocated by: %s", p.poolOfSchemas.allocMap[k]) - outcome = false - } - for k, v := range p.poolOfResults.debugMap { - if v == statusRedeemed { - continue - } - t.Logf("result should be redeemed. Allocated by: %s", p.poolOfResults.allocMap[k]) - outcome = false - } - - return outcome -} - -func caller() string { - pc, _, _, _ := runtime.Caller(3) //nolint:dogsled - from, line := runtime.FuncForPC(pc).FileLine(pc) - - return fmt.Sprintf("%s:%d", from, line) -} diff --git a/pools_debug_test.go b/pools_debug_test.go deleted file mode 100644 index 9ca8a59..0000000 --- a/pools_debug_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers -// SPDX-License-Identifier: Apache-2.0 - -//go:build validatedebug - -package validate - -import ( - "path/filepath" - "testing" - - "github.com/go-openapi/loads" - "github.com/go-openapi/strfmt" - "github.com/go-openapi/testify/v2/require" -) - -func Test_Debug_2866(t *testing.T) { - // This test to be run with build flag "validatedebug": it uses the debug pools and asserts that - // all allocated objects are indeed redeemed at the end of the spec validation. - - resetPools() - fp := filepath.Join("fixtures", "bugs", "2866", "2866.yaml") - - doc, err := loads.Spec(fp) - require.NoError(t, err) - require.NotNil(t, doc) - - require.NoError(t, Spec(doc, strfmt.Default)) - - require.True(t, pools.allIsRedeemed(t)) -} diff --git a/pools_leak_test.go b/pools_leak_test.go new file mode 100644 index 0000000..b6cbd5a --- /dev/null +++ b/pools_leak_test.go @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package validate + +import ( + "encoding/json" + "path/filepath" + "testing" + + "github.com/go-openapi/loads" + "github.com/go-openapi/spec" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag/pools" + "github.com/go-openapi/testify/v2/require" +) + +// TestPools_NoLeakOnSpecValidation asserts that validating a specification +// gives back every object it borrowed. +// +// The assertion only bites when built with the "poolsdebug" tag, which turns +// on borrow tracking; without it the test still runs the validation, and the +// pools panic on nothing. Run the instrumented build with: +// +// go test -tags poolsdebug ./... +func TestPools_NoLeakOnSpecValidation(t *testing.T) { + resetPools() + pools.ResetTracking() + t.Cleanup(pools.ResetTracking) + + fp := filepath.Join("fixtures", "bugs", "2866", "2866.yaml") + doc, err := loads.Spec(fp) + require.NoError(t, err) + require.NotNil(t, doc) + + require.NoError(t, Spec(doc, strfmt.Default)) + + require.True(t, pools.AssertNoLeaks(t)) +} + +// TestPools_NoLeakOnSchemaValidation covers the other entry point that +// recycles: validating data against a schema, rather than a whole spec. +func TestPools_NoLeakOnSchemaValidation(t *testing.T) { + resetPools() + pools.ResetTracking() + t.Cleanup(pools.ResetTracking) + + schema := new(spec.Schema) + require.NoError(t, json.Unmarshal([]byte(`{ + "type": "object", + "required": ["name"], + "properties": { + "name": {"type": "string"}, + "friends": {"type": "array", "items": {"type": "object", "required": ["age"]}}, + "either": {"oneOf": [{"type": "string"}, {"type": "integer"}]} + } + }`), schema)) + + const friendsProp = "friends" + + for _, data := range []any{ + map[string]any{nameProp: "ok", friendsProp: []any{map[string]any{"age": 1}}, "either": "s"}, + map[string]any{friendsProp: []any{map[string]any{}}, "either": true}, + nil, + } { + _ = AgainstSchema(schema, data, strfmt.Default) + } + + require.True(t, pools.AssertNoLeaks(t)) +} + +// TestPools_NoLeakOnNilSchema exercises the path that hands back the shared +// empty result, which the pool must not be given. +func TestPools_NoLeakOnNilSchema(t *testing.T) { + resetPools() + pools.ResetTracking() + t.Cleanup(pools.ResetTracking) + + require.NoError(t, AgainstSchema(nil, map[string]any{}, strfmt.Default)) + require.True(t, pools.AssertNoLeaks(t)) +} diff --git a/result.go b/result.go index fc6f8e7..a4834b4 100644 --- a/result.go +++ b/result.go @@ -134,7 +134,7 @@ func (r *Result) Merge(others ...*Result) *Result { r.mergeWithoutRootSchemata(other) r.rootObjectSchemata.Append(other.rootObjectSchemata) if other.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(other) + redeemResult(other) } } return r @@ -201,7 +201,7 @@ func (r *Result) MergeAsErrors(others ...*Result) *Result { r.carryErrors(other.Warnings, other.warningLocations) r.MatchCount += other.MatchCount if other.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(other) + redeemResult(other) } } } @@ -219,7 +219,7 @@ func (r *Result) MergeAsWarnings(others ...*Result) *Result { r.carryWarnings(other.Warnings, other.warningLocations) r.MatchCount += other.MatchCount if other.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(other) + redeemResult(other) } } } @@ -443,7 +443,7 @@ func (r *Result) mergeForField(obj map[string]any, field string, other *Result) }) } if other.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(other) + redeemResult(other) } return r @@ -471,7 +471,7 @@ func (r *Result) mergeForSlice(slice reflect.Value, i int, other *Result) *Resul } if other.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(other) + redeemResult(other) } return r @@ -574,7 +574,7 @@ func (r *Result) keepRelevantErrors() *Result { } var strippedResult *Result if r.wantsRedeemOnMerge { - strippedResult = pools.poolOfResults.BorrowResult() + strippedResult = validatorPools.results.Borrow() } else { strippedResult = new(Result) } @@ -585,6 +585,14 @@ func (r *Result) keepRelevantErrors() *Result { return strippedResult } +// Reset clears this result so it may be reused, keeping allocated capacity. +// +// It implements the hook the pool calls when a result is borrowed and when it +// is redeemed. Calling it on a result still in use loses its findings. +func (r *Result) Reset() { + _ = r.cleared() +} + func (r *Result) cleared() *Result { // clear the Result to be reusable. Keep allocated capacity. r.Errors = r.Errors[:0] diff --git a/result_location_test.go b/result_location_test.go index 11d209b..6ccb7c1 100644 --- a/result_location_test.go +++ b/result_location_test.go @@ -97,7 +97,7 @@ func TestResultLocations_DedupeKeepsTheFirstLocation(t *testing.T) { func TestResultLocations_ClearedOnRecycle(t *testing.T) { t.Parallel() - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() res.addErrorsAt(newPathSegments("a"), errOne) require.Len(t, res.LocatedErrors(), 1) diff --git a/schema.go b/schema.go index 2dfa39a..e7af892 100644 --- a/schema.go +++ b/schema.go @@ -42,7 +42,7 @@ func AgainstSchema(schema *spec.Schema, data any, formats strfmt.Registry, optio append(options, WithRecycleValidators(true), withRecycleResults(true))..., ).Validate(data) defer func() { - pools.poolOfResults.RedeemResult(res) + redeemResult(res) }() if res.HasErrors() { @@ -100,7 +100,7 @@ func newSchemaValidator(schema *spec.Schema, rootSchema any, root pathSegments, var s *SchemaValidator if opts.recycleValidators { - s = pools.poolOfSchemaValidators.BorrowValidator() + s = validatorPools.schemaValidators.Borrow() } else { s = new(SchemaValidator) } @@ -158,7 +158,7 @@ func (s *SchemaValidator) Validate(data any) *Result { var result *Result if s.Options.recycleResult { - result = pools.poolOfResults.BorrowResult() + result = validatorPools.results.Borrow() result.data = data } else { result = &Result{data: data} @@ -366,7 +366,7 @@ func (s *SchemaValidator) setPath(path pathSegments) { } func (s *SchemaValidator) redeem() { - pools.poolOfSchemaValidators.RedeemValidator(s) + validatorPools.schemaValidators.Redeem(s) } func (s *SchemaValidator) redeemChildren() { diff --git a/schema_props.go b/schema_props.go index 08a2030..83a3735 100644 --- a/schema_props.go +++ b/schema_props.go @@ -56,7 +56,7 @@ func newSchemaPropsValidator( var s *schemaPropsValidator if opts.recycleValidators { - s = pools.poolOfSchemaPropsValidators.BorrowValidator() + s = validatorPools.schemaPropsValidators.Borrow() } else { s = new(schemaPropsValidator) } @@ -87,7 +87,7 @@ func (s *schemaPropsValidator) Applies(source any, _ reflect.Kind) bool { func (s *schemaPropsValidator) Validate(data any) *Result { var mainResult *Result if s.Options.recycleResult { - mainResult = pools.poolOfResults.BorrowResult() + mainResult = validatorPools.results.Borrow() } else { mainResult = new(Result) } @@ -107,17 +107,17 @@ func (s *schemaPropsValidator) Validate(data any) *Result { } if len(s.anyOfValidators) > 0 { - keepResultAnyOf = pools.poolOfResults.BorrowResult() + keepResultAnyOf = validatorPools.results.Borrow() s.validateAnyOf(data, mainResult, keepResultAnyOf) } if len(s.oneOfValidators) > 0 { - keepResultOneOf = pools.poolOfResults.BorrowResult() + keepResultOneOf = validatorPools.results.Borrow() s.validateOneOf(data, mainResult, keepResultOneOf) } if len(s.allOfValidators) > 0 { - keepResultAllOf = pools.poolOfResults.BorrowResult() + keepResultAllOf = validatorPools.results.Borrow() s.validateAllOf(data, mainResult, keepResultAllOf) } @@ -150,7 +150,7 @@ func (s *schemaPropsValidator) validateAnyOf(data any, mainResult, keepResultAny if result.IsValid() { if bestFailures != nil && bestFailures.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(bestFailures) + redeemResult(bestFailures) } _ = keepResultAnyOf.cleared() @@ -162,7 +162,7 @@ func (s *schemaPropsValidator) validateAnyOf(data any, mainResult, keepResultAny // MatchCount is used to select errors from the schema with most positive checks if bestFailures == nil || result.MatchCount > bestFailures.MatchCount { if bestFailures != nil && bestFailures.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(bestFailures) + redeemResult(bestFailures) } bestFailures = result @@ -170,7 +170,7 @@ func (s *schemaPropsValidator) validateAnyOf(data any, mainResult, keepResultAny } if result.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(result) // this result is ditched + redeemResult(result) // this result is ditched } } @@ -201,7 +201,7 @@ func (s *schemaPropsValidator) validateOneOf(data any, mainResult, keepResultOne if firstSuccess == nil { firstSuccess = result } else if result.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(result) // this result is ditched + redeemResult(result) // this result is ditched } continue @@ -210,11 +210,11 @@ func (s *schemaPropsValidator) validateOneOf(data any, mainResult, keepResultOne // MatchCount is used to select errors from the schema with most positive checks if validated == 0 && (bestFailures == nil || result.MatchCount > bestFailures.MatchCount) { if bestFailures != nil && bestFailures.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(bestFailures) + redeemResult(bestFailures) } bestFailures = result } else if result.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(result) // this result is ditched + redeemResult(result) // this result is ditched } } @@ -226,13 +226,13 @@ func (s *schemaPropsValidator) validateOneOf(data any, mainResult, keepResultOne case 1: mainResult.Merge(firstSuccess) if bestFailures != nil && bestFailures.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(bestFailures) + redeemResult(bestFailures) } default: mainResult.addErrorsAt(s.Path, mustValidateOnlyOneSchemaMsg(s.Path.dotted(), fmt.Sprintf("Found %d valid alternatives", validated))) mainResult.Merge(bestFailures) if firstSuccess != nil && firstSuccess.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(firstSuccess) + redeemResult(firstSuccess) } } } @@ -273,7 +273,7 @@ func (s *schemaPropsValidator) validateNot(data any, mainResult *Result) { mainResult.addErrorsAt(s.Path, mustNotValidatechemaMsg(s.Path.dotted())) } if result.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(result) // this result is ditched + redeemResult(result) // this result is ditched } } @@ -307,7 +307,7 @@ func (s *schemaPropsValidator) setPath(path pathSegments) { } func (s *schemaPropsValidator) redeem() { - pools.poolOfSchemaPropsValidators.RedeemValidator(s) + validatorPools.schemaPropsValidators.Redeem(s) } func (s *schemaPropsValidator) redeemChildren() { diff --git a/slice_validator.go b/slice_validator.go index 3445dad..0a0ee74 100644 --- a/slice_validator.go +++ b/slice_validator.go @@ -34,7 +34,7 @@ func newSliceValidator(path pathSegments, in string, var v *schemaSliceValidator if opts.recycleValidators { - v = pools.poolOfSliceValidators.BorrowValidator() + v = validatorPools.sliceValidators.Borrow() } else { v = new(schemaSliceValidator) } @@ -68,7 +68,7 @@ func (s *schemaSliceValidator) Validate(data any) *Result { var result *Result if s.Options.recycleResult { - result = pools.poolOfResults.BorrowResult() + result = validatorPools.results.Borrow() } else { result = new(Result) } @@ -137,5 +137,5 @@ func (s *schemaSliceValidator) setPath(path pathSegments) { } func (s *schemaSliceValidator) redeem() { - pools.poolOfSliceValidators.RedeemValidator(s) + validatorPools.sliceValidators.Redeem(s) } diff --git a/spec.go b/spec.go index 5568d38..f2ced4c 100644 --- a/spec.go +++ b/spec.go @@ -178,7 +178,7 @@ func (s *SpecValidator) SetContinueOnErrors(c bool) { } func (s *SpecValidator) validateNonEmptyPathParamNames() *Result { - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() if s.spec.Spec().Paths == nil { // There is no Paths object: error res.addErrorsAt(newPathSegments(swaggerPaths), noValidPathMsg()) @@ -212,7 +212,7 @@ func (s *SpecValidator) validateDuplicateOperationIDs() *Result { // fallback on possible incomplete picture because of previous errors analyzer = s.analyzer } - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() known := make(map[string]int) for _, v := range analyzer.OperationIDs() { if v != "" { @@ -234,7 +234,7 @@ type dupProp struct { func (s *SpecValidator) validateDuplicatePropertyNames() *Result { // definition can't declare a property that's already defined by one of its ancestors - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() for k, sch := range s.spec.Spec().Definitions { if len(sch.AllOf) == 0 { continue @@ -283,7 +283,7 @@ func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, schn := nm schc := &sch - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() for schc.Ref.String() != "" { // gather property names @@ -320,7 +320,7 @@ func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, } func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, knowns map[string]struct{}) ([]string, *Result) { - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() if sch.Ref.String() == "" && len(sch.AllOf) == 0 { // Safeguard. We should not be able to actually get there return nil, res @@ -371,7 +371,7 @@ func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, kno //nolint:gocognit // refactor in a forthcoming PR func (s *SpecValidator) validateItems() *Result { // validate parameter, items, schema and response objects for presence of item if type is array - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() for method, pi := range s.analyzer.Operations() { for path, op := range pi { @@ -436,7 +436,7 @@ func (s *SpecValidator) validateItems() *Result { // Verifies constraints on array type. func (s *SpecValidator) validateSchemaItems(schema spec.Schema, at pathSegments, prefix, opID string) *Result { - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() if !schema.Type.Contains(arrayType) { return res } @@ -460,7 +460,7 @@ func (s *SpecValidator) validateSchemaItems(schema spec.Schema, at pathSegments, func (s *SpecValidator) validatePathParamPresence(path string, fromPath, fromOperation []string) *Result { // Each defined operation path parameters must correspond to a named element in the API's path pattern. // (For example, you cannot have a path parameter named id for the following path /pets/{petId} but you must have a path parameter named petId.) - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() for _, l := range fromPath { var matched bool for _, r := range fromOperation { @@ -513,7 +513,7 @@ func (s *SpecValidator) validateReferencedParameters() *Result { if len(expected) == 0 { return nil } - result := pools.poolOfResults.BorrowResult() + result := validatorPools.results.Borrow() for k := range expected { result.addWarningsAt(localRefPath(k), unusedParamMsg(k)) } @@ -538,7 +538,7 @@ func (s *SpecValidator) validateReferencedResponses() *Result { if len(expected) == 0 { return nil } - result := pools.poolOfResults.BorrowResult() + result := validatorPools.results.Borrow() for k := range expected { result.addWarningsAt(localRefPath(k), unusedResponseMsg(k)) } @@ -573,7 +573,7 @@ func (s *SpecValidator) validateReferencedDefinitions() *Result { func (s *SpecValidator) validateRequiredDefinitions() *Result { // Each property listed in the required array must be defined in the properties of the model - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() DEFINITIONS: for d, schema := range s.spec.Spec().Definitions { @@ -607,7 +607,7 @@ func (s *SpecValidator) validateRequiredProperties( path, in string, schemaAt, requiredAt pathSegments, v *spec.Schema, ) *Result { // Takes care of recursive property definitions, which may be nested in additionalProperties schemas - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() propertyMatch := false patternMatch := false additionalPropertiesMatch := false @@ -674,7 +674,7 @@ func (s *SpecValidator) validateParameters() *Result { // - parameters with pattern property must specify valid patterns // - $ref in parameters must resolve // - path param must be required - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() rexGarbledPathSegment := mustCompileRegexp(`.*[{}\s]+.*`) for method, pi := range s.expandedAnalyzer().Operations() { methodPaths := make(map[string]map[string]string) @@ -816,7 +816,7 @@ func (s *SpecValidator) validateParameters() *Result { func (s *SpecValidator) validateReferencesValid() *Result { // each reference must point to a valid object - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() for _, r := range s.analyzer.AllRefs() { if !r.IsValidURI(s.spec.SpecFilePath()) { // Safeguard - spec should always yield a valid URI res.addErrorsAt(s.refLocations.at(r.String()), invalidRefMsg(r.String())) @@ -845,7 +845,7 @@ func (s *SpecValidator) checkUniqueParams(path, method string, op *spec.Operatio // However, there are some issues with such a factorization: // - analysis does not seem to fully expand params // - param keys may be altered by x-go-name - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() pnames := make(map[string]struct{}) if op.Parameters != nil { // Safeguard diff --git a/spec_ref_warnings.go b/spec_ref_warnings.go index a499a64..3c8608f 100644 --- a/spec_ref_warnings.go +++ b/spec_ref_warnings.go @@ -34,7 +34,7 @@ const minDistinctHostsToWarn = 2 // // All findings are warnings: they do not affect validity (see Result.IsValid). func (s *SpecValidator) validateDubiousRefs() *Result { - res := pools.poolOfResults.BorrowResult() + res := validatorPools.results.Borrow() baseDir, hasBase := s.localBaseDir() diff --git a/type.go b/type.go index 9baa3d6..3016783 100644 --- a/type.go +++ b/type.go @@ -30,7 +30,7 @@ func newTypeValidator(path pathSegments, in string, typ spec.StringOrArray, null var t *typeValidator if opts.recycleValidators { - t = pools.poolOfTypeValidators.BorrowValidator() + t = validatorPools.typeValidators.Borrow() } else { t = new(typeValidator) } @@ -202,5 +202,5 @@ func (t *typeValidator) setPath(path pathSegments) { } func (t *typeValidator) redeem() { - pools.poolOfTypeValidators.RedeemValidator(t) + validatorPools.typeValidators.Redeem(t) } diff --git a/validator.go b/validator.go index 46a32ca..3989b21 100644 --- a/validator.go +++ b/validator.go @@ -39,7 +39,7 @@ func newItemsValidator(path pathSegments, in string, items *spec.Items, root any var iv *itemsValidator if opts.recycleValidators { - iv = pools.poolOfItemsValidators.BorrowValidator() + iv = validatorPools.itemsValidators.Borrow() } else { iv = new(itemsValidator) } @@ -73,7 +73,7 @@ func (i *itemsValidator) Validate(index int, data any) *Result { kind := tpe.Kind() var result *Result if i.Options.recycleResult { - result = pools.poolOfResults.BorrowResult() + result = validatorPools.results.Borrow() } else { result = new(Result) } @@ -193,7 +193,7 @@ func (i *itemsValidator) formatValidator() valueValidator { } func (i *itemsValidator) redeem() { - pools.poolOfItemsValidators.RedeemValidator(i) + validatorPools.itemsValidators.Redeem(i) } func (i *itemsValidator) redeemChildren() { @@ -226,7 +226,7 @@ func newBasicCommonValidator(path pathSegments, in string, def any, enum []any, var b *basicCommonValidator if opts.recycleValidators { - b = pools.poolOfBasicCommonValidators.BorrowValidator() + b = validatorPools.basicCommonValidators.Borrow() } else { b = new(basicCommonValidator) } @@ -282,7 +282,7 @@ func (b *basicCommonValidator) setPath(path pathSegments) { } func (b *basicCommonValidator) redeem() { - pools.poolOfBasicCommonValidators.RedeemValidator(b) + validatorPools.basicCommonValidators.Redeem(b) } // A HeaderValidator has very limited subset of validations to apply. @@ -311,7 +311,7 @@ func newHeaderValidator(name string, header *spec.Header, formats strfmt.Registr var p *HeaderValidator if opts.recycleValidators { - p = pools.poolOfHeaderValidators.BorrowValidator() + p = validatorPools.headerValidators.Borrow() } else { p = new(HeaderValidator) } @@ -354,7 +354,7 @@ func (p *HeaderValidator) Validate(data any) *Result { var result *Result if p.Options.recycleResult { - result = pools.poolOfResults.BorrowResult() + result = validatorPools.results.Borrow() } else { result = new(Result) } @@ -460,7 +460,7 @@ func (p *HeaderValidator) formatValidator() valueValidator { } func (p *HeaderValidator) redeem() { - pools.poolOfHeaderValidators.RedeemValidator(p) + validatorPools.headerValidators.Redeem(p) } func (p *HeaderValidator) redeemChildren() { @@ -503,7 +503,7 @@ func newParamValidator(param *spec.Parameter, formats strfmt.Registry, opts *Sch var p *ParamValidator if opts.recycleValidators { - p = pools.poolOfParamValidators.BorrowValidator() + p = validatorPools.paramValidators.Borrow() } else { p = new(ParamValidator) } @@ -538,7 +538,7 @@ func (p *ParamValidator) Validate(data any) *Result { var result *Result if p.Options.recycleResult { - result = pools.poolOfResults.BorrowResult() + result = validatorPools.results.Borrow() } else { result = new(Result) } @@ -652,7 +652,7 @@ func (p *ParamValidator) formatValidator() valueValidator { } func (p *ParamValidator) redeem() { - pools.poolOfParamValidators.RedeemValidator(p) + validatorPools.paramValidators.Redeem(p) } func (p *ParamValidator) redeemChildren() { @@ -695,7 +695,7 @@ func newBasicSliceValidator( var s *basicSliceValidator if opts.recycleValidators { - s = pools.poolOfBasicSliceValidators.BorrowValidator() + s = validatorPools.basicSliceValidators.Borrow() } else { s = new(basicSliceValidator) } @@ -762,7 +762,7 @@ func (s *basicSliceValidator) Validate(data any) *Result { return err } if err.wantsRedeemOnMerge { - pools.poolOfResults.RedeemResult(err) + redeemResult(err) } } } @@ -775,7 +775,7 @@ func (s *basicSliceValidator) setPath(path pathSegments) { } func (s *basicSliceValidator) redeem() { - pools.poolOfBasicSliceValidators.RedeemValidator(s) + validatorPools.basicSliceValidators.Redeem(s) } type numberValidator struct { @@ -805,7 +805,7 @@ func newNumberValidator( var n *numberValidator if opts.recycleValidators { - n = pools.poolOfNumberValidators.BorrowValidator() + n = validatorPools.numberValidators.Borrow() } else { n = new(numberValidator) } @@ -866,7 +866,7 @@ func (n *numberValidator) Validate(val any) *Result { var res, resMultiple, resMinimum, resMaximum *Result if n.Options.recycleResult { - res = pools.poolOfResults.BorrowResult() + res = validatorPools.results.Borrow() } else { res = new(Result) } @@ -879,7 +879,7 @@ func (n *numberValidator) Validate(val any) *Result { res.addErrorsAt(n.Path, IsValueValidAgainstRange(val, n.Type, n.Format, "Checked", n.Path.dotted())) if n.MultipleOf != nil { - resMultiple = pools.poolOfResults.BorrowResult() + resMultiple = validatorPools.results.Borrow() // Is the constraint specifier within the range of the specific numeric type and format? resMultiple.addErrorsAt(n.Path, IsValueValidAgainstRange(*n.MultipleOf, n.Type, n.Format, "MultipleOf", n.Path.dotted())) @@ -897,7 +897,7 @@ func (n *numberValidator) Validate(val any) *Result { } if n.Maximum != nil { - resMaximum = pools.poolOfResults.BorrowResult() + resMaximum = validatorPools.results.Borrow() // Is the constraint specifier within the range of the specific numeric type and format? resMaximum.addErrorsAt(n.Path, IsValueValidAgainstRange(*n.Maximum, n.Type, n.Format, "Maximum boundary", n.Path.dotted())) @@ -915,7 +915,7 @@ func (n *numberValidator) Validate(val any) *Result { } if n.Minimum != nil { - resMinimum = pools.poolOfResults.BorrowResult() + resMinimum = validatorPools.results.Borrow() // Is the constraint specifier within the range of the specific numeric type and format? resMinimum.addErrorsAt(n.Path, IsValueValidAgainstRange(*n.Minimum, n.Type, n.Format, "Minimum boundary", n.Path.dotted())) @@ -942,7 +942,7 @@ func (n *numberValidator) setPath(path pathSegments) { } func (n *numberValidator) redeem() { - pools.poolOfNumberValidators.RedeemValidator(n) + validatorPools.numberValidators.Redeem(n) } type stringValidator struct { @@ -968,7 +968,7 @@ func newStringValidator( var s *stringValidator if opts.recycleValidators { - s = pools.poolOfStringValidators.BorrowValidator() + s = validatorPools.stringValidators.Borrow() } else { s = new(stringValidator) } @@ -1038,5 +1038,5 @@ func (s *stringValidator) setPath(path pathSegments) { } func (s *stringValidator) redeem() { - pools.poolOfStringValidators.RedeemValidator(s) + validatorPools.stringValidators.Redeem(s) } diff --git a/validator_test.go b/validator_test.go index 1d85741..13914ec 100644 --- a/validator_test.go +++ b/validator_test.go @@ -48,7 +48,7 @@ func TestHeaderValidator(t *testing.T) { require.NotNil(t, res) require.Empty(t, res.Errors) require.TrueT(t, res.wantsRedeemOnMerge) - pools.poolOfResults.RedeemResult(res) + redeemResult(res) }) }) } From 5540162954ab2ee3d72364e95f1dbbf89462f7b9 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sun, 9 Aug 2026 11:08:44 +0200 Subject: [PATCH 2/2] ci: disabled the parallel pool testing on windows and darwin Parallel test with -race exhausts CI runners on windows and darwin. The path it is testing doesn't need multi-platform tests, so only linux will validate. Signed-off-by: Frederic BIDON --- pools_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pools_test.go b/pools_test.go index a6bc48f..1e27631 100644 --- a/pools_test.go +++ b/pools_test.go @@ -1,3 +1,5 @@ +//go:build !windows && !darwin + // SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 @@ -14,6 +16,10 @@ import ( ) func Test_ParallelPool(t *testing.T) { + // Hitting more stringent memory & threads constraints on windows, when running -race, so we disable this on + // that target OS. Typically, a CI runner breaks with "ThreadSanitizer failed to allocate ..." + // Also -race in this context times out on macos. We need our validation on our platform only. + fixture1 := filepath.Join("fixtures", "bugs", "1429", "swagger.yaml") fixture2 := filepath.Join("fixtures", "bugs", "2866", "2866.yaml") fixture3 := filepath.Join("fixtures", "bugs", "43", "fixture-43.yaml")