Back MemoryStore with a real MongoDB when available, fall back to mongomock-ng - #1121
Back MemoryStore with a real MongoDB when available, fall back to mongomock-ng#1121rkingsbury wants to merge 6 commits into
Conversation
…gomock-ng MemoryStore now probes host:port (default localhost:27017) on connect and, if a MongoDB server answers, stores data in a real, ephemeral MongoDB database for full MongoDB compatibility and performance. Each instance uses a unique maggma_memory_<uuid> database that is dropped automatically (via weakref finalizer) when the store is garbage collected or the interpreter exits, so the user never has to set up or clean up a database. If no server is reachable the store transparently falls back to an in-process mongomock-ng database. - close() is now a no-op that leaves the store usable, preserving the historical mongomock behavior relied upon by the builders (pymongo raises on use-after-close). Use connect(force_reset=True) to discard contents. - JSONStore/FileStore inherit the behavior via a shared _connect_collection; MontyStore gets its own close() so its disk-backed client still closes. - Adds backward-compatible host/port/mongoclient_kwargs/server_selection_timeout_ms kwargs; server_selection_timeout_ms=0 forces the mongomock backend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1121 +/- ##
==========================================
+ Coverage 69.53% 69.78% +0.24%
==========================================
Files 47 47
Lines 4103 4160 +57
==========================================
+ Hits 2853 2903 +50
- Misses 1250 1257 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Addresses GitHub AI security warnings about except clauses that only pass without an explanatory comment (the two best-effort ephemeral-database drops). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@Andrew-S-Rosen any thoughts on this? I feel this is the best solution to allowing |
|
Supersedes #846 |
Previously MemoryStore.close() was a no-op so that callers could keep querying a "closed" store. This makes close() actually disconnect: querying a closed store now raises StoreError, and the store must be reconnected first. Data is preserved across a close()/connect() cycle -- the real-MongoDB backend keeps it in the ephemeral server-side database (dropped by the finalizer on GC/exit), and the mongomock backend keeps its in-process client alive -- so reconnect restores the contents while connect(force_reset=True) starts fresh. The Builder framework already only queries stores before closing them (run/serial/multi all do connect -> get_items -> update_targets -> finalize, and MapBuilder.finalize queries before super().finalize()). The only query-after-close was in the builder test suite, which relied on the old no-op close(); those tests now reconnect the target before asserting, matching the existing pattern in test_copy_builder.test_run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test_close expected an AttributeError from querying a closed S3Store (because S3Store.close() sets s3_bucket=None). With MemoryStore.close() now being a genuine close, the S3 index is genuinely closed and raises StoreError when queried, before the s3_bucket access is reached. Expect StoreError, which is the more principled signal that the store was closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@utf I'd also value your input, given |
|
Hi @rkingsbury, I had an initial look at the PR and made a few tests. Since we use the Here are some comments:
I also have a question related to #1115. Having the limitation on the pymongo version was definitely an issue and it is good that it has been resolved. However, I see that the |
MemoryStore previously probed localhost:27017 on every connect and used a real MongoDB server if one answered, falling back to mongomock otherwise. Make the real backend strictly opt-in instead: - The `port` kwarg now defaults to None, in which case the Store always uses the in-process mongomock backend -- no probing and no dependency on a running server, even if one happens to be listening on localhost. - Supplying a port (e.g. port=27017 for a local mongod) opts in to a real MongoDB server at host:port, writing to the ephemeral, auto-dropped database. Removes the auto-probe/ping/fallback path; server_selection_timeout_ms now only controls the real client's timeout. Tests and docstrings updated accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Great feedback @gpetretto, thank you for testing!
You're absolutely right. I've modified the PR so that this is now an opt-in featured. If the new kwarg
Again thank you for the detailed explanation of the errors. Maybe we (read: Claude :) can add some warnings or guard statements to verify the DB is read/writable and make this failure clearer.
Yes I appreciate your thinking about this. I do not know the developer; however based on this issue and a few others in the base FWIW, I did ask Claude to research some alternatives to |
Summary
Building on #1115 (mongomock → mongomock-ng), this lets
MemoryStoreopt in to a real, ephemeral MongoDB backend when one is available, while keeping the in-processmongomockbackend as the default — with no manual database setup either way.Motivation (see #830, #788): mongomock is a reimplementation and doesn't cover every MongoDB feature. When a real
mongodis available, opting in gives exact MongoDB semantics and better performance, while the default keeps working out-of-the-box with no server installed.Behavior
MemoryStoretakes an optionalport:port=None(default) → data is held in an in-processmongomockdatabase. No server is required or contacted, even if one happens to be listening on localhost. This is the historical behavior.port=<int>(e.g.port=27017) → the Store uses a real MongoDB server athost:port, writing to a database namedmaggma_memory_<uuid>, unique per store instance so instances never collide. That database is dropped automatically (via aweakref.finalize) when the store is garbage collected or the interpreter exits, so nothing is left behind and the user never creates or cleans up a database manually.Other new, backward-compatible kwargs:
host(used only when a port is supplied),mongoclient_kwargs,server_selection_timeout_ms(the real client's timeout). ExistingMemoryStore("name")usage is unchanged and still uses mongomock.close() semantics
MemoryStore.close()is a genuine close: after it, the store must be reconnected withconnect()before it can be queried again (querying a closed store raisesStoreError). Data is preserved across aclose()/connect()cycle — the real-MongoDB backend keeps it in the ephemeral server-side database, and the mongomock backend keeps its in-process client alive — whileconnect(force_reset=True)discards the contents and starts fresh.The build pipeline already only queries stores before closing them:
Builder.run()and theserial/multiprocessingrunners all doconnect → get_items → update_targets → finalize(), andMapBuilder.finalize()queries beforesuper().finalize(). A couple of builder tests previously queried the target afterrun()had closed it; they now reconnect first (matching the existing pattern intest_copy_builder.test_run).Other notes
JSONStoreandFileStoreinherit the behavior through the shared connection helper (and use the default mongomock backend);MontyStorekeeps its ownclose().test_jsonstore_orjson_optionstest relies on the mongomock backend, since a real MongoDB coerces the customfloatsubclass before write and theserialization_defaultoption under test would never trigger (this is the backend-dependent case discussed in MemoryStore: replace mongomock with pymongo-inmemory #846).Testing
tests/stores/test_mongolike.py,tests/stores/test_file_store.py,tests/stores/test_aws.py,tests/builderswith a realmongodrunning: 74 passed, 9 skipped, 1 xfailed, with no leftovermaggma_memory_*databases. (The default-mongomock test runs with a server present and confirms the real backend is strictly opt-in.)as_dict/from_dict,MontyDecoder, anddeepcopyroundtrips still work.🤖 Generated with Claude Code