Skip to content

Back MemoryStore with a real MongoDB when available, fall back to mongomock-ng - #1121

Open
rkingsbury wants to merge 6 commits into
materialsproject:mainfrom
rkingsbury:memorystore-auto-mongo
Open

Back MemoryStore with a real MongoDB when available, fall back to mongomock-ng#1121
rkingsbury wants to merge 6 commits into
materialsproject:mainfrom
rkingsbury:memorystore-auto-mongo

Conversation

@rkingsbury

@rkingsbury rkingsbury commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Building on #1115 (mongomock → mongomock-ng), this lets MemoryStore opt in to a real, ephemeral MongoDB backend when one is available, while keeping the in-process mongomock backend 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 mongod is available, opting in gives exact MongoDB semantics and better performance, while the default keeps working out-of-the-box with no server installed.

Behavior

MemoryStore takes an optional port:

  • port=None (default) → data is held in an in-process mongomock database. 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 at host:port, writing to a database named maggma_memory_<uuid>, unique per store instance so instances never collide. That database is dropped automatically (via a weakref.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). Existing MemoryStore("name") usage is unchanged and still uses mongomock.

close() semantics

MemoryStore.close() is a genuine close: after it, the store must be reconnected with connect() before it can be queried again (querying a closed store raises StoreError). Data is preserved across a close()/connect() cycle — the real-MongoDB backend keeps it in the ephemeral server-side database, and the mongomock backend keeps its in-process client alive — while connect(force_reset=True) discards the contents and starts fresh.

The build pipeline already only queries stores before closing them: Builder.run() and the serial/multiprocessing runners all do connect → get_items → update_targets → finalize(), and MapBuilder.finalize() queries before super().finalize(). A couple of builder tests previously queried the target after run() had closed it; they now reconnect first (matching the existing pattern in test_copy_builder.test_run).

Other notes

  • JSONStore and FileStore inherit the behavior through the shared connection helper (and use the default mongomock backend); MontyStore keeps its own close().
  • The test_jsonstore_orjson_options test relies on the mongomock backend, since a real MongoDB coerces the custom float subclass before write and the serialization_default option 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/builders with a real mongod running: 74 passed, 9 skipped, 1 xfailed, with no leftover maggma_memory_* databases. (The default-mongomock test runs with a server present and confirms the real backend is strictly opt-in.)
  • Verified the port-supplied backend: querying a closed store raises, reconnect restores the data, and the ephemeral database is dropped by the finalizer; and that as_dict/from_dict, MontyDecoder, and deepcopy roundtrips still work.

🤖 Generated with Claude Code

…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>
Comment thread src/maggma/stores/mongolike.py Fixed
Comment thread src/maggma/stores/mongolike.py Fixed
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.09677% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.78%. Comparing base (cf97834) to head (d3c0939).

Files with missing lines Patch % Lines
src/maggma/stores/mongolike.py 87.09% 8 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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>
@rkingsbury

Copy link
Copy Markdown
Collaborator Author

@Andrew-S-Rosen any thoughts on this? I feel this is the best solution to allowing MemoryStore to more closely emulate true mongo behavior, rather than relying on mocking. Basically, if mongod is running, you'll get a real Mongo collection in memory. Otherwise, mongomock-ng will be used.

@rkingsbury

Copy link
Copy Markdown
Collaborator Author

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>
@rkingsbury

Copy link
Copy Markdown
Collaborator Author

@utf I'd also value your input, given maggma's use in atomate2

@gpetretto

Copy link
Copy Markdown
Contributor

Hi @rkingsbury,

I had an initial look at the PR and made a few tests. Since we use the JSONStore in jobflow-remote this might have an impact on us as well.

Here are some comments:

  • from what I tested, if the local DB exists but is password protected and the credentials are not set, the MemoryStore just sets _using_real_mongo to True, but then it fails when it tries to insert/access some documents. Maybe it would be good to also check if the DB is accessible with read/write permission before setting _using_real_mongo?

  • when the failure happens in inserting a new document the error is difficult to interpret because the methods in the base class MongoStore tries to handle it, but uses attributes that are not defined in the subclass (i.e. safe_update)

    OperationFailure: Command update requires authentication, full error: {'ok': 0.0, 'errmsg': 'Command update requires authentication', 'code': 13, 'codeName': 'Unauthorized'}

    During handling of the above exception, another exception occurred:

    AttributeError Traceback (most recent call last)
    Cell In[11], line 1
    ----> 1 ms.update({"test":1, "task_id": 1})

    File ~/tmp/maggma/src/maggma/stores/mongolike.py:399, in MongoStore.update(self, docs, key)
    397 self._collection.bulk_write(requests, ordered=False)
    398 except (OperationFailure, DocumentTooLarge) as e:
    --> 399 if self.safe_update:
    400 for req in requests:
    401 try:

    AttributeError: 'MemoryStore' object has no attribute 'safe_update'

    In this context it seems problematic that the __init__ of the MongoStore is not called. Not sure if it would be possible to actually call it, to ensure that all the attributes are properly set.

  • Unless for an existing passwordless local mongodb, the basic configuration would not allow to connect. I wonder if it is a common occurrence for a user to have such a configuration. Wouldn't it make more sense that the local mongodb is an opt-in rather than an opt-out option?

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 mongomock-ng package is a fork with 2 stars on github and is from a user with relatively little github contributions. Of course I mean no disrespect to the developer, that could be a reliable experienced developer, but I was wondering if you trust the repository and/or if you know the developer.
I am asking because, as mentioned above, the JSONStore is heavily used in jobflow-remote and it cannot rely on a real mongodb.

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>
@rkingsbury

Copy link
Copy Markdown
Collaborator Author

Great feedback @gpetretto, thank you for testing!

Unless for an existing passwordless local mongodb, the basic configuration would not allow to connect. I wonder if it is a common occurrence for a user to have such a configuration. Wouldn't it make more sense that the local mongodb is an opt-in rather than an opt-out option?

You're absolutely right. I've modified the PR so that this is now an opt-in featured. If the new kwarg port=None (default), you get the existing / legacy behavior. Only if port is set will maggma try to use a real DB for the Store.

Maybe it would be good to also check if the DB is accessible with read/write permission before setting _using_real_mongo?

when the failure happens in inserting a new document the error is difficult to interpret because the methods in the base class MongoStore tries to handle it, but uses attributes that are not defined in the subclass (i.e. safe_update)

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.

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 mongomock-ng package is a fork with 2 stars on github and is from a user with relatively little github contributions. Of course I mean no disrespect to the developer, that could be a reliable experienced developer, but I was wondering if you trust the repository and/or if you know the developer.
I am asking because, as mentioned above, the JSONStore is heavily used in jobflow-remote and it cannot rely on a real mongodb.

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 mongomock repo, my understanding is they have now been invited to take over maintenance of mongomock itself; I think the fork is sort of temporary until longer-term plans for mongomock solidify. While you rightly raise questions about the long-term reliability / robustness, in my opinion it is no worse than the previous state of affairs in which mongomock was more or less unmaintained. And since mongomock-ng is the fork of the future maintainer, it seems that it will be a good predictor of how mongomock itself will evolve.

FWIW, I did ask Claude to research some alternatives to mongomock, but each of those identified had some limitations that made them less desirable than sticking with mongomock.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants