Repository navigation
Let FlowData and pipelines release native memory, and close them in the tests and examples - #36
Merged
Merged
Conversation
The resource cleanup feature of the pipeline specification says Element Data and Flow Elements holding native resources must have a cleanup mechanism, that disposing Flow Data cleans up its Element Data, and that the Pipeline cleans up its Flow Elements. None of these existed in Python. FlowData refers to itself through its evidence and a pipeline through its elements, so neither is freed by reference counting, and the native results and engines they hold stayed in memory until the cycle collector happened to run. The results hold the engine's data set, so the data stayed after the pipeline went away. - FlowData.close() closes the element data that has a close method of its own, and leaves the rest alone so cached element data still works. FlowData is a context manager. - Pipeline.close() closes its flow elements, unless the pipeline is built with auto_close_elements set to False because the elements are shared with another pipeline. Pipeline is a context manager. - FlowElement.close() does nothing. Engine.close() stops the checks and updates of the engine's data file through the new DataFileUpdateService.unregister_data_file(), because the update and file watcher timers otherwise hold the engine for the life of the process. - The device detection and IP intelligence on-premise results release their native results on close, and reading a property afterwards raises an exception. Their engines release the native engine on close, and processing afterwards raises an exception. - ShareUsage.close() shuts down its thread pool. Nothing changes for code that never calls close.
The tests and examples built on-premise pipelines and FlowData and never released them, so their native results, engines and data sets stayed in memory until the cycle collector ran, and pipelines with automatic updates or a file watcher were held by their timers until the process ended. Tests now close what they create with addCleanup, or with tearDownClass and tearDownModule for pipelines shared by several tests. Examples use with statements, so that they show how the pipeline and each FlowData are meant to be released. The test that hashes the update archive closes the file it opens. New tests check that closing a FlowData releases its results, that closing a pipeline releases its engine, and that closing a pipeline cancels the engine's next automatic update.
A conftest.py in each test folder that runs on-premise engines turns the cycle collector off while each test runs, then fails the test if any native results or engine it created is still alive. A FlowData or pipeline that is not closed only goes when the collector runs, so with the collector off anything a test leaves open is certain to be found, wherever in the test it was created. Objects created before a test, such as a pipeline built in setUpClass, are checked once every test has run, when the session fails if any is still alive.
The tox environments of the device detection and IP intelligence on-premise and examples packages installed their packages from the working copy, but took the pipeline packages they require from PyPI. Their tests therefore ran against the last released pipeline code, not the code in the same change, and could not use anything new in it, such as closing a FlowData or a pipeline. These four environments now install the pipeline packages from the checkout as well. Only the test environments change. The published packages and their requirements are the same as before.
…kages The device detection integration tests install the examples package in a virtual environment of its own to run the web examples under the Selenium contract tests. That environment took the pipeline packages from PyPI, so the examples ran against the last released pipeline code. It now installs them from the checkout, as the tox environments do. The device detection packages there still come from PyPI, as before.
The on-premise packages release their native memory through FlowData.close(), Pipeline.close() and Engine.close(), which are first in fiftyone_pipeline_core and fiftyone_pipeline_engines 4.5.42, so they and the examples packages now require those versions. Both pipeline packages were last released as 4.5.41, and the release plan in ci/packages.ps1 releases a changed package at the next patch, so 4.5.42 carries close() as long as this change is in the next release of the two packages. A checkout's packages have no version.txt and install as 0.0.0, which is below those minimums, so ci/write-package-versions.ps1 gives each published package the version this commit would release it at, and the root setup-environment.ps1 runs it before anything is installed.
When an element in a configuration fails to build, or the pipeline cannot be made from the elements, build_from_configuration now closes the elements it has already built before the error is raised, so their native memory and data file updates are released. A failure to close one is logged, and the error that stopped the build is the one raised. A new test builds a configuration whose second element fails, and checks that the first was closed and that the original error came out.
rc now reads the data files from the data repository in cxx/data. The one conflict was in the IP intelligence getting started web example, where rc's message about finding the data file is kept, and the pipeline is still closed with a with statement when the server stops.
Merged
jwrosewell
marked this pull request as ready for review
November 4, 2026 21:00
jwrosewell
added a commit
that referenced
this pull request
Nov 4, 2026
Releases what has collected on `rc` since #30: - [#26](#26) Update submodules - [#28](#28) Read the data files from the data repository in cxx/data - [#34](#34) Say in AGENTS.md how to get the data, build and run the examples - [#35](#35) Point package metadata and package READMEs at the right pages - [#32](#32) Ask the country property for its no value message in the location example - [#29](#29) Describe the rc approach to releases in AGENTS.md - [#31](#31) Check the callback URL before the JavaScript builder writes it - [#36](#36) Let FlowData and pipelines release native memory, and close them in the tests and examples ## Release note From [#36](#36): - `FlowData` and `Pipeline` now have `close()` and work in a `with` statement. Closing a FlowData releases the native results its element data holds. Closing a pipeline closes its flow elements, unless it was built with `auto_close_elements` set to false. - The on-premise Device Detection and IP Intelligence packages now require `fiftyone_pipeline_core>=4.5.42` and `fiftyone_pipeline_engines>=4.5.42`, the versions this release publishes with `close()`. ## 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 09:23 UTC on 4 November 2026.
This was referenced Nov 4, 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 feature of the pipeline specification says Element Data and Flow Elements that hold native (C/C++) resources must have a cleanup mechanism, that cleaning up Flow Data cleans up its Element Data, and that the Pipeline cleans up its Flow Elements. The required examples must show that every Flow Data is disposed of.
None of this existed in Python. There was no way to release the native results of an on-premise engine, its native engine, or a pipeline. Reference counting did not free them either, because a FlowData refers to itself through its evidence and a pipeline refers to itself through its elements, so they went only when the cycle collector happened to run. The native results hold the engine's data set, so a data set stayed in memory after its pipeline had gone, for as long as any of its results were still waiting for the collector. The automatic update and file watcher timers of the data file update service could not be stopped at all, and each held its pipeline and engine until the process ended.
The fix in the library
FlowData.close()closes the element data that has aclosemethod of its own and leaves the rest alone, so cached element data keeps working, as the specification requires. FlowData is a context manager.Pipeline.close()closes its flow elements, unless the pipeline is built withauto_close_elementsset toFalsebecause its elements are shared with another pipeline, which is the option the specification asks for when elements can be in several pipelines. Pipeline is a context manager, andPipelineBuildertakes the setting asauto_close_elements.FlowElement.close()does nothing.Engine.close()stops the checks and updates of the engine's data file through the newDataFileUpdateService.unregister_data_file(), which cancels the pending timers so that nothing holds the engine.SwigData) release their native results when closed, and reading a property afterwards raises an exception that says so. The on-premise engines release the native engine when closed, and processing afterwards raises an exception that says so.ShareUsage.close()shuts down its thread pool.PipelineBuilder.build_from_configuration()closes the elements it has already built when a later one fails to build, or when the pipeline cannot be made from them, and then raises the error that stopped the build. A failure to close one is logged.Nothing changes for code that never calls
close. No native code and no SWIG interface changed.The bugs in the tests and examples
Every one of these created an on-premise pipeline or FlowData and released it on no path, including the exception paths. Line numbers are from
rcbefore this change.Tests, 7 files:
device-detection/fiftyone_devicedetection_onpremise/tests/test_devicedetection.py: 32 tests built a pipeline that was never closed (the builds end at lines 63 to 640), and 23 of them also processed a FlowData that was never closed (lines 114 to 642). The test at line 73 also left an automatic update timer holding the engine for the rest of the run. The unused helper at line 654 created a FlowData for each of 20,000 User-Agents without closing any.device-detection/fiftyone_devicedetection_onpremise/tests/test_automatic_data_file_updates.py: 11 tests each built a pipeline and processed a FlowData, closing neither (lines 70 to 257), and line 328 opened the archive without closing it.device-detection/fiftyone_devicedetection_onpremise/tests/test_properties.py: the module's pipeline (line 43) and 3 FlowData (lines 52, 77 and 112).device-detection/fiftyone_devicedetection_onpremise/tests/test_swig.py: the module's pipeline (line 40).device-detection/fiftyone_devicedetection_examples/tests/test_onpremisegettingstartedweb.py: the pipeline of the web example the tests build (line 40).ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_ipintelligence.py: the pipeline built insetUpClass(line 48), the FlowData of the 5 tests that use theprocesshelper (line 53), and the pipeline and FlowData at lines 211 and 215.ip-intelligence/fiftyone_ipintelligence_examples/tests/test_examples.py: the pipeline and FlowData at lines 67 and 73.Examples, 22 files, each with a pipeline and FlowData that were never closed:
device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/:gettingstarted_console.py(lines 69 and 86),match_metrics.py(90 and 94),metadata_console.py(78),offlineprocessing.py(100, and one FlowData per record at 143),performance.py(93, and one FlowData per User-Agent at 66),failuretomatch.py(34, 39 and 67),datafileupdate_console.py(two pipelines at 236 and 297, the second held for good by its automatic update and file watcher timers),shareusage.py(131 and 160),gettingstarted_web/app.py(98, and one FlowData per request at 115, 140 and 168) anduseragentclienthints_web/app.py(72, and one FlowData per request at 87).device-detection/fiftyone_devicedetection/examples/gettingstarted.py(33, 41 and 67) anddevice-detection/fiftyone_devicedetection_examples/performance-tests/test_performance.py(74, and one FlowData per detection at 51).ip-intelligence/fiftyone_ipintelligence_examples/src/fiftyone_ipintelligence_examples/onpremise/:gettingstarted_console.py(92 and 107),compare_console.py(97 and 121),countries_console.py(72 and 81),metadata_console.py(48),mixed_console.py(141 and 148),offlineprocessing.py(66 and 87),performance_console.py(a pipeline per worker at 118 and 159, and one FlowData per record at 87),datafileupdate_console.py(176, a pipeline with automatic updates and a file watcher that was not even kept in a variable) andgettingstarted_web/app.py(63, and one FlowData per request at 92).ip-intelligence/fiftyone_ipintelligence/examples/gettingstarted.py(33, 41 and 67).Tests now close what they create with
addCleanup, or withtearDownClassandtearDownModulefor pipelines that several tests share. Examples usewithstatements, with a comment saying what closing releases, so that they show the lifetime the specification describes. New tests check that closing a FlowData releases its results, that closing a pipeline releases its engine, that closing a pipeline cancels the engine's next automatic update, and howFlowData.close(),Pipeline.close()andunregister_data_file()behave. A test ofbuild_from_configuration()builds a configuration whose second element fails, and checks that the first was closed and that the original error was raised.The cloud tests and examples, and the tests of the pipeline packages, are unchanged, because their FlowData hold no native resources and so have nothing to release.
The tests now use the pipeline code in the change
The tox environments of the device detection and IP intelligence on-premise and examples packages installed those packages from the checkout, but took the pipeline packages they require from PyPI, so their tests ran against the last released pipeline code rather than the code in the same change, and could not see
close. The device detection integration tests also run the web examples under the Selenium contract tests from a virtual environment of their own, which took the pipeline packages from PyPI too. Both now install the pipeline packages from the checkout. The device detection packages in that virtual environment still come from PyPI, as before.The packages require the pipeline versions with
closeThe device detection and IP intelligence on-premise packages now require
fiftyone_pipeline_core>=4.5.42andfiftyone_pipeline_engines>=4.5.42, and the two examples packages requirefiftyone_pipeline_core>=4.5.42, so updating one of them also brings pipeline packages that haveclose. The on-premise packages need the engines minimum as well, because thecloseof their engines callsEngine.close(), which stops the data file timers that would otherwise keep the engine alive. The examples packages are never published, so their requirement only records what they need.Each package here is versioned and tagged on its own, as
<package>/<version>, and the release plan inci/packages.ps1releases a package that changed since its last tag at the next patch. The newest tags arefiftyone_pipeline_core/4.5.41andfiftyone_pipeline_engines/4.5.41, and this change touches both packages, so the release that includes it publishes both at 4.5.42. Run at the head of this pull request,Get-ReleasePlangives 4.5.42 for both, each "changed since 4.5.41".The number holds only if this change is in the next release of those two packages.
rcalready changesfiftyone_pipeline_coresince 4.5.41, through itsjs_templatessubmodule, so ifrcis released before this change is merged, 4.5.42 goes out withoutcloseand both minimums must move to 4.5.43. The publish prints its plan under "This publish releases:" before it builds anything.A package's
setup.pyreads its version from aversion.txtthat the publish writes, so a package installed from a checkout is 0.0.0, which is below the new minimums. pip would then find no pipeline package that meets them, or replace the checkout's with a release from PyPI and test that instead. The newci/write-package-versions.ps1gives each published package the version this commit would release it at, or its last release when this commit would not release it, and the rootci/setup-environment.ps1runs it before anything is installed.fiftyone_pipeline_didkeeps theversion.txtit commits. Git now ignoresversion.txt, andAGENTS.mdsays to run the script before running those tests from a checkout.The guard
A
conftest.pyin each of the four test folders that run on-premise engines turns the cycle collector off while each test runs, then fails the test if any native results or native engine it created is still alive. A FlowData or pipeline that is not closed is only freed by the collector, so with the collector off, anything a test or an example leaves open is certain to be found, wherever it was created. Objects created before a test, such as a pipeline built insetUpClassor when a module is imported, are checked after the last test, and the session fails if any is still alive. A test that has already failed is not reported again.No lint rule can do this. pylint's
consider-using-withand ruff'sSIM115only know a fixed list of standard library calls, native memory is not visible totracemalloc, and the process size is too noisy to measure it by.How it was checked
rcwas merged in at 61bcb09, afterrctook the move of the data files tocxx/data. The one conflict was in the IP intelligence getting started web example, which keepsrc's message about finding the data file and still closes the pipeline with awithstatement when the server stops.The full build ran common-ci's pull request build of this pull request with dryrun, so nothing was merged, and the other builds ran it with pull request 0 and dryrun, on a throwaway branch per run that has since been deleted.
fiftyone_pipeline_engines_fiftyone, which do not callclose. The rerun of that job passed in 24 minutes, about the time the other macOS jobs took.rcwas merged in, and passed on all 10 Ubuntu and Windows configurations, Python 3.10 to 3.14, with 184 pipeline core tests passing on each.build_from_configuration()as it was before, on Ubuntu with Python 3.13. The new test failed with "AssertionError: 1 != 0", because nothing closed the first element, and the other 183 pipeline core tests passed.ci/write-package-versions.ps1gavefiftyone_pipeline_coreandfiftyone_pipeline_engines4.5.42, and pip installed the checkout's pipeline packages at those versions.test_basic_get_onpremise, and the guard failed that test at teardown with "1 native object(s) still alive (ResultsHashSwig)".test_onpremise_gettingstarted_consolewith "4 native object(s) still alive (ResultsIpiSwig, ResultsIpiSwig, ResultsIpiSwig, ResultsIpiSwig)".fiftyone_pipeline_core4.5.41 from PyPI, which has noclose, and the second because the Selenium environment did.Release note
FlowDataandPipelinenow haveclose()and work in awithstatement. Closing a FlowData releases the native results its element data holds. Closing a pipeline closes its flow elements, unless it was built withauto_close_elementsset to false.fiftyone_pipeline_core>=4.5.42andfiftyone_pipeline_engines>=4.5.42, the versions this release publishes withclose().