Repository navigation
Close every FlowData, Pipeline and metadata collection, and fail the build when one is left open - #44
Merged
Merged
Conversation
…y path The servlet filter closed a request's FlowData only when the rest of the filter chain returned normally, and the result service lost the FlowData when processing it failed. The Spring interceptor closed it in afterCompletion, which Spring does not call when preHandle returns false, so the FlowData of every request for the JavaScript or JSON was never closed. Java never frees native results by itself, so each of those requests kept its native results, and through them the engine's data set, until the process ended. ProfileMetaData.getName iterated the native values collection without closing it, which held the data set until the garbage collector finalized the collection.
…ts and examples The pipeline builder tests built 38 on-premise pipelines, each with its data file loaded, and closed none of them. Other tests left FlowData, pipelines, share usage elements and their sending threads, and metadata collections holding the data set, open on some or all paths. Several examples built engines into a pipeline that was not asked to close them, or closed the pipeline outside a finally block.
…ft open ci/pmd-resource-ruleset.xml holds three PMD rules, run in every tree after the tests compile. A FlowData must be created in a try-with-resources statement, a metadata collection an engine returns must be used in one, and a Pipeline a builder has built must be closed in one or in a finally block, unless each is returned to the caller.
…e the exceptions closing can throw
When an element failed to build, or the pipeline could not be built from the elements, the elements already built were left open, holding their data files and threads. They are now closed before the failure is passed on. The original exception is the one thrown, and any failure to close an element is added to it as suppressed.
… FlowData once in the web examples ComponentMetaData.getProperties(), ProfileMetaData.getValues(), ProfileMetaData.getValues(String) and FiftyOneAspectPropertyMetaData.getValues() declared Iterable, while the on-premise engines return native collections from them, two of which hold the data set until closed. They now declare CloseableIterable, so a caller can close the collection without a cast, and the callers here close it. The IP Intelligence web examples closed the FlowData that the filter owns and closes at the end of the request. They no longer close it, and their comments say that the filter does.
Merged
…t tests rc now has #40, whose renderWithHosts helper in JavaScriptUrlTests builds a Pipeline outside a try-with-resources statement and never closes it. The PipelineClosed rule this pull request adds reports that, so the helper now builds the pipeline in a try-with-resources statement, as render in the same file does.
jwrosewell
marked this pull request as ready for review
November 4, 2026 23:29
jwrosewell
added a commit
that referenced
this pull request
Nov 4, 2026
Releases what has collected on `rc` since #30: - [#37](#37) Use the data repository for the examples, tests and CI - [#41](#41) Say in AGENTS.md how to get the data, build and run the examples - [#42](#42) Point package metadata at this repository and fix stale links - [#39](#39) Describe the rc approach to releases in AGENTS.md - [#40](#40) Check the callback URL before the JavaScript builder writes it - [#44](#44) Close every FlowData, Pipeline and metadata collection, and fail the build when one is left open ## Release note From [#44](#44): - `ComponentMetaData.getProperties()`, `ProfileMetaData.getValues()`, `ProfileMetaData.getValues(String)` and `FiftyOneAspectPropertyMetaData.getValues()` now declare `CloseableIterable` rather than `Iterable`, which is what the on-premise engines have always returned. Close the collection, for example in a try-with-resources statement, so that it releases the engine's data set straight away. Source that assigns the result to an `Iterable` still compiles. Code compiled against the old declared types needs recompiling, and a class that implements one of these interfaces needs its method to declare `CloseableIterable`, so this is a small breaking change. The release stays a patch release. - `PipelineBuilder.buildFromConfiguration` now closes the elements it has already built when building fails. - The servlet filter and the Spring interceptor now close the request's FlowData on every path. ## Notes The cxx submodule moves from [4.5.102](https://github.com/51Degrees/51degrees-cxx/releases/tag/4.5.102) to [4.5.103](https://github.com/51Degrees/51degrees-cxx/releases/tag/4.5.103). That release adds the [data](https://github.com/51Degrees/data) repository at `cxx/data` and changes nothing under `src/`. No commit asks for a major or minor version change, so the publish moves only patch numbers. The last release into `main` was #30, merged at 06:35 UTC on 4 November 2026.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
The resource cleanup specification says that disposing a FlowData cleans up its element data, that element data and engines holding native memory must have a cleanup mechanism, and that the Pipeline cleans up the elements it contains. The required examples must show that every FlowData is disposed.
In Java the native results inside a FlowData are never freed by the garbage collector. The SWIG interface gives results, evidence, values and vectors no finalizer, so they are freed only when the FlowData is closed. Each native result holds the engine's data set and its resource manager, so one FlowData left open keeps the whole data set in memory after the Pipeline and the engine are closed, for as long as the process runs. The metadata collections an engine returns (profiles, components, values) hold the data set in the same way until they are closed, or until the garbage collector finalizes them.
A Java pipeline only closes its elements when it is built with
setAutoCloseElements(true), which the pre-packaged builders andbuildFromConfigurationdo butnew PipelineBuilder()does not.What was found and fixed
In the library (7)
pipeline.web/.../PipelineFilter.java(124 to 146)pipeline.web/.../services/PipelineResultServiceCore.java(75 to 84)pipeline.web.mvc/.../components/FiftyOneInterceptor.java(125 to 139)preHandlethrew. Spring callsafterCompletiononly whenpreHandlereturns true.preHandlenow closes it in every other case.device-detection.hash.engine.on-premise/.../data/ProfileMetaDataHash.java(117)getNameiterates.ip-intelligence.engine.on-premise/.../data/ProfileMetaDataIPI.java(117)pipeline.core/.../flowelements/PipelineBuilder.java,buildFromConfiguration(83 to 138)ComponentMetaData.getProperties(),ProfileMetaData.getValues(),ProfileMetaData.getValues(String),FiftyOneAspectPropertyMetaData.getValues()Iterablewhile the on-premise engines return native collections, two of which hold the data set, so a caller could only close one by casting. They now declareCloseableIterable, and every caller here closes it. See the release note below.The declared types of those four methods are the only change to the public API.
In the examples (11)
examples/device-detection/console/.../MetadataOnPrem.java(139, 148)examples/device-detection/console/.../ShareUsageExample.java(139)examples/ip-intelligence/console/.../GettingStartedCloudMixed.java(147)examples/ip-intelligence/web/getting-started.api/.../GettingStartedApi.java(126 to 137)initfailed, as a servlet whoseinitfails is never destroyed.location/geo-location.shell.examples/.../CombiningServices.java(151 to 176)location/geo-location.shell.examples/.../cloud/GettingStarted.java(95 to 119)pipeline.developer-examples.cloud-engine/.../Main.java(108),flowelement/.../Main.java(48),onpremise-engine/.../Main.java(50),usage-sharing/.../Main.java(92)Each example now builds its pipeline in a try-with-resources statement, and asks the pipeline to close the elements it was given. The metadata examples close the property and value collections as well.
The four IP Intelligence web examples (
GettingStartedWebOnPrem,GettingStartedWebCloud,GettingStartedWebMixed,GettingStartedWebCloudMixed) closed the FlowData that the filter owns and closes at the end of the request, although their comments said not to. They no longer close it, and their comments now say that the filter does.In the tests (106)
device-detection/.../DeviceDetectionOnPremisePipelineBuilderTest.java(23 tests, 90 to 321)ip-intelligence/.../IPIntelligenceOnPremisePipelineBuilderTest.java(91 to 255, 162)pipeline.cloudrequestengine/.../CloudRequestEngineTests.java(78, 80, 116, 120, 333, 453, 482)pipeline.cloudrequestengine/.../CloudAspectEngineBaseTests.java(373, 425, 483)pipeline.core/.../FlowDataTests.java(323),ParallelElementsTests.java(129),PipelineTests.java(105)pipeline.core/.../PipelineBuilderTests.java(419, 446, 484)pipeline.core/.../integration/PipelineIntegrationTests.java(57, 74, 94, 116)pipeline.jsonbuilder/.../JsonBuilderTests.java(463)pipeline.javascriptbuilder/.../CookieTests.java(141),JavaScriptUrlTests.java(74)device-detection/.../CloudRequestOriginTests.java(89),ip-intelligence/.../CloudRequestOriginTests.java(91)pipeline.engines.fiftyone/.../SetHeadersElementTests.java(87, 107, 142, 170, 187, 210)pipeline.engines.fiftyone/.../ShareUsageElementTests.java(16 tests, 253 to 1006)pipeline.engines.fiftyone/.../ShareUsageOverheadTests.java(114, 152)pipeline.web/.../PipelineFilterTests.java(3 tests)device-detection.hash.engine.on-premise/.../EngineTests.java,ip-intelligence.engine.on-premise/.../EngineTests.java(65)device-detection.hash.engine.on-premise/.../data/DataValidatorHash.java(94).../data/MetaDataHasherHash.javain both on-premise engines (67, 79, 96 and 62, 74, 91) anddevice-detection.shared/.../data/ValueTests.java(70)CloseableIterable, so the callers can close them.The guard
ci/pmd-resource-ruleset.xmladds three PMD rules, and every tree's pom runs them withmaven-pmd-pluginonce the tests are compiled, so the build fails on a new leak. The IP Intelligence tree runs them beside its existing unused code check.FlowDataInTryWithResourcesfails when a FlowData is created bycreateFlowData()ornew FlowDataDefault(...)outside a try-with-resources statement, unless it is returned or kept in a field.CloseableIterableInTryWithResourcesfails when a call returning aCloseableIterable, such as an engine's profiles or components, is used outside a try-with-resources statement, unless it is returned.PipelineClosedfails when a local Pipeline a builder has just built is neither a try-with-resources resource, nor closed in a finally block of the same method, nor returned.The rules match how resources are created, so a mocked FlowData or Pipeline, or one borrowed from the web integration, is not reported. PMD's own
CloseResourcerule was tried first and left out. It reported every mock, and it treats a pipeline as closed whenever the pipeline is named in a try-with-resources statement, sotry (FlowData data = pipeline.createFlowData())hid a pipeline that was never closed. Where something is closed by its owner elsewhere, the code says so with@SuppressWarnings. That happens twice, where the result service hands its FlowData to the request, and where the overhead tests close their FlowData together so that closing is not timed.The rules do not see a resource kept in a field, or a pipeline built by a helper method.
How it was checked
Nothing was built or run locally. Every check below ran on GitHub's runners through common-ci's pull request build (
nightly-pull-request.yml, with the dry run on so that nothing is merged), on a throwaway branch carrying the named head with the macOS configurations taken out ofci/options.json, as the macOS runners were needed for package publishing at the time.CloseResourcerule reporting every unclosedAutoCloseable(run 41818499245, 409 reports, most of them mocks or objects owned elsewhere), then builds reporting the three rules above without failing (41819956080, 41829473215, 41830100340 and, after the metadata types changed, 41870221674) until none was left.rcmoved. Head69b631ab, built and tested with the rules failing the build, run 41871146691 passed on all 9 Linux and Windows configurations. Its first attempt failed on Ubuntu with Java 8 in the Selenium contract tests, where Firefox did not start, and the second attempt of the failed and cancelled jobs passed.8cb65cf0, which merges the currentrc(the data repository, AGENTS.md and package metadata changes) into this branch, run 41886371194 passed on all 9 Linux and Windows configurations at the first attempt.pipeline.engines.fiftyoneone test that builds a Pipeline and never closes it, creates 100 FlowData without closing them, and iterates an engine's profiles without closing the collection. On Ubuntu with Java 17, run 41851717793 failed the build inpipeline.engines.fiftyonewith exactly three violations,PipelineClosed,FlowDataInTryWithResourcesandCloseableIterableInTryWithResources, one for each leak, after every module built before it had passed the check. The rules have not changed since.The full build with the macOS configurations is run 41901498556.
Release note
ComponentMetaData.getProperties(),ProfileMetaData.getValues(),ProfileMetaData.getValues(String)andFiftyOneAspectPropertyMetaData.getValues()now declareCloseableIterablerather thanIterable, which is what the on-premise engines have always returned. Close the collection, for example in a try-with-resources statement, so that it releases the engine's data set straight away. Source that assigns the result to anIterablestill compiles. Code compiled against the old declared types needs recompiling, and a class that implements one of these interfaces needs its method to declareCloseableIterable, so this is a small breaking change. The release stays a patch release.PipelineBuilder.buildFromConfigurationnow closes the elements it has already built when building fails.Not changed
new PipelineBuilder()keeps its default of not closing the elements it is given unless asked.DeviceDataTestsandIPIntelligenceDataTests, hold nothing native and are unchanged.