Conversation
…ge-count fast path; bidirectional node query edge limit; ObjectPager keyset/limited fetch; auto-create indexes default on outside serverless; fix async_transaction_context to pass Mongo txn handle; added index_partial_filter_expression capability
…ge-count fast path; bidirectional node query edge limit; ObjectPager keyset/limited fetch; auto-create indexes default on outside serverless; fix async_transaction_context to pass Mongo txn handle; added index_partial_filter_expression capability
…erformance monitoring
…d first-class support for internal directory/sandbox marker files in storage validation and path rules
Multi-phase community-readiness pass covering durability, query
push-down, caching, observability, bulk APIs, throttle resilience,
benchmarks, and contributor-facing scaffolding.
Phase A1 -- IO durability
* Crash-safe atomic writes (temp + fsync + rename + dir-fsync) in
jvspatial/db/_atomic.py; wired into JsonDB and LocalFileInterface
(versioned-content writes are now atomic per write, ordered for
recoverable crash modes).
* Per-path PathLockManager (bounded LRU) replaces the global single
threading.Lock in JsonDB; writes to different files run in
parallel, same-file writes serialize. Cross-thread safe.
* Honest transaction semantics: JsonDBTransaction is now strict by
default (raises NotImplementedError); opt-in best_effort=True
buffers writes/deletes in memory and applies on commit. Database
gains a supports_transactions capability flag.
Phase A2 -- Native count + filter pushdown
* Database.count() native on every backend.
- SQLite: SELECT COUNT(*) with a Mongo->SQL translator
(jvspatial/db/_sqlite_translate.py) that pushes
$eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$exists, top-level AND, and
$and/$or (recursive) into json_extract() WHERE clauses with
LIMIT/ORDER BY pushdown. Falls back to legacy in-Python filter
for $regex/$elemMatch/etc.
- MongoDB: count_documents / estimated_document_count.
- DynamoDB: Select="COUNT" via Scan or GSI Query.
- JsonDB: dirent fast path for empty queries; streaming match for
filtered queries (no result-list materialization).
* Entity-layer Object.count() / Object.find() benefit transparently.
Phase A3 -- Caching
* QueryEngine optimization cache bounded by an LRU
(DEFAULT_QUERY_CACHE_SIZE=1024).
* Opt-in CachingDatabase wrapper (jvspatial/db/_cache.py) with
LRU+TTL get-cache, negative caching, save/delete/find-one-and-*
invalidation. Wired via create_database(cache_get_size=,
cache_get_ttl=). Skipped under serverless mode.
* SQLite connection lifecycle documented (single persistent
connection per instance + WAL; no pool needed).
Phase A4 -- Bulk APIs
* Database.find_many(ids) and Database.bulk_save(records) added to
the protocol with native overrides on every backend (Mongo $in /
bulk_write, SQLite single-transaction IN / executemany, DynamoDB
BatchGetItem/BatchWriteItem, JsonDB parallel reads/writes).
* CachingDatabase splits find_many between cache hits and a single
backend call for misses.
* DeferredSaveMixin gains max_pending_saves auto-flush bound.
* Fixed a concurrency bug in JsonDB's lazy orphan-tmp sweep that
raced concurrent writers (sweep is now gated by a threading.Lock
so it runs exactly once before any write proceeds).
Phase A5 -- Resilience
* Shared async retry helper (jvspatial/utils/retry.py) with
exponential backoff + full jitter, configurable retryable
predicate, on_retry hook.
* MongoDB.save/get/delete/find refactored to use the shared helper;
semantics preserved (one retry on connection-error with reset).
* DynamoDB.save/get/delete auto-retry on throttle codes
(ProvisionedThroughputExceededException, ThrottlingException,
RequestLimitExceeded, TooManyRequestsException,
TransactionConflictException).
* S3 storage: multipart upload at >= 8 MiB (configurable via
constructor or JVSPATIAL_S3_MULTIPART_THRESHOLD env);
save/get/delete retry on SlowDown/RequestTimeout/5xx.
Phase B1/B2 -- Observability
* MetricsRecorder Protocol (jvspatial/observability/metrics.py) with
NullMetricsRecorder default (zero overhead).
* OpenTelemetry adapter under [otel] extra
(jvspatial/observability/otel.py).
* ObservableDatabase wrapper (jvspatial/db/_observable.py) emits a
structured log line + 4 metrics per DB op
(jvspatial.db.op.{duration_seconds, count, slow_count,
result_count}). Slow-query elevation to WARNING. Wired via
create_database(observe=True, slow_query_ms=, metrics=).
* Composes after the cache so log timings reflect user-visible
latency including cache hits/misses.
Phase B3 -- Benchmarks
* tests/benchmarks/ with pytest-benchmark; 13 benches across
JsonDB / SQLite / DeferredSave guarding the IO wins.
* .github/workflows/benchmarks.yml runs on PRs, downloads the
latest bench-baseline artifact (published on main pushes), and
posts a markdown comparison comment.
* docs/md/benchmarks.md.
Phase C1 -- Community scaffolding
* CONTRIBUTING.md (root), CODE_OF_CONDUCT.md (Contributor
Covenant 2.1 by reference), RELEASING.md aligned with the
publish.yml workflow.
* py.typed PEP 561 marker shipped via pyproject.toml package data.
Phase C2 -- CI hardening
* .github/dependabot.yml for pip + github-actions ecosystems.
* .github/workflows/security.yml runs pip-audit weekly + on dep
changes.
* Coverage gate raised 55 -> 60%.
Phase C3 -- Public API discipline
* @experimental decorator (jvspatial/utils/stability.py) and
@deprecated decorator (jvspatial/utils/deprecation.py): once-
per-process warnings, async support, serverless suppression.
* JsonDBTransaction(best_effort=True) emits an ExperimentalWarning.
* docs/md/stability.md declaring public/internal/experimental
tiers and deprecation policy.
Phase C4 -- Docs
* docs/md/observability.md, docs/md/benchmarks.md,
docs/md/stability.md.
* README cross-links to the new pages and the contributor surface.
Tests
* +12 new test files, ~110+ new test cases covering atomic writes,
path locks, transaction semantics, local storage atomicity,
query translator, SQLite pushdown, JsonDB count, caching
database, observable database, metrics recorder, OpenTelemetry
adapter, bulk APIs, deferred-save auto-flush, retry helper, S3
multipart, stability marker, deprecation marker.
Downstream impact
* jvagent and integral audited: zero callers of the only
meaningfully-breaking surface (JsonDBTransaction direct usage).
No coordinated change required in either repo.
Note: pre-commit was not run in the commit-creation environment
(no PyPI access). Run `pre-commit run --all-files` locally and
amend if any formatter (black/isort) rewrites land.
CI workflows
- benchmarks.yml: gate compare/comment steps on baseline file presence
so PRs without a prior 'main' bench-baseline don't fail the job;
emit a notice that the baseline will appear on next push to main.
- security.yml: pip-audit runs with '--skip-editable' so the in-tree
jvspatial editable install (no PyPI presence yet) doesn't always
fail; '--strict' still gates on real CVEs in transitive deps.
Test fixes
- tests/core/test_deferred_save_auto_flush.py:
* Annotate _AutoFlushNode.max_pending_saves as ClassVar[Optional[int]];
Pydantic v2 rejects un-annotated non-field attrs on BaseModel
subclasses.
* Object.create() calls save() then flush(); flush() turns deferred
mode off and clears _pending_save_count. Tests must call
enable_deferred_saves() after create() to start a fresh batching
session (otherwise subsequent save()s write directly and is_dirty
stays False).
- tests/storage/test_s3_multipart.py:
patch.object(cls, name) replaces with a MagicMock, which is *not* a
descriptor and therefore doesn't auto-bind self when invoked via an
instance. side_effect=_set was being called with no args, yielding
'TypeError: _set() missing 1 required positional argument'.
Replace with a real lambda (descriptor-friendly) and assign
s3_client on the constructed instance instead.
- tests/db/test_caching_database.py::test_find_one_and_delete_invalidates:
Save with both 'id' and '_id' fields so the {"_id": "x"} query
matches under JsonDB (which doesn't auto-mirror id<->_id at find
time). Aligns with test_find_one_and_update_refreshes which already
uses both fields.
The default FileValidator blocks the .bin extension (it's in BLOCKED_EXTENSIONS alongside .exe/.dll/.app/.run). The multipart routing tests were uploading 'small.bin'/'big.bin'/'threshold.bin' and the validator raised before the upload code decided put_object vs upload_fileobj, so all routing assertions failed. Validator behavior is correct -- the fixtures just need an allowed extension. .txt with the same byte payload detects as text/plain (in DEFAULT_ALLOWED_MIME_TYPES), so the upload reaches the routing branch as intended. No production code change.
Previously the workflow did 'pip install ".[dev,test,otel]"' (non- editable). pip-audit then saw jvspatial in the resolved env as a regular wheel and tried to look it up on PyPI -- the in-development 0.0.x isn't published, so the audit failed with 'Dependency not found on PyPI: jvspatial (0.0.7)' even with --skip-editable in place. Switch to 'pip install -e' so --skip-editable actually excludes jvspatial from the resolution. Audit still runs against the full resolved transitive set -- only jvspatial itself is skipped.
…heck
The previous attempt installed jvspatial editable and passed
--skip-editable to pip-audit, but --skip-editable is unreliable
across pip-audit versions: the 'distribution marked as editable'
error still fires on the same input ('jvspatial: distribution
marked as editable').
Switch to feeding pip-audit an explicit requirements list generated
from the resolved env with jvspatial filtered out. This side-steps
both failure modes:
* Non-editable install -> 'Dependency not found on PyPI' (jvspatial
0.0.x isn't published yet)
* Editable install -> 'distribution marked as editable' even with
--skip-editable
Audit coverage is unchanged: the full transitive dependency set
still gets scanned, only jvspatial itself is skipped.
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.
Type of Change
What type of change does this PR introduce? Mark all that apply:
Summary
What does this PR address?
Multi-phase community-readiness pass to harden jvspatial's IO layer, expose first-class observability and resilience primitives, ship contributor-facing scaffolding, and fix a concurrency bug discovered in flight. After this PR, jvspatial is durable under crashes, faster on every backend, opt-in observable, and structurally ready for outside contributors.
count()+ filter/sort/limit pushdown across MongoDB, SQLite, DynamoDB, JsonDBDatabase.find_many()andDatabase.bulk_save()with native per-backend overridesCachingDatabase(LRU + TTL, negative caching) andObservableDatabase(structured log + metrics) wrappers, composable viacreate_database()kwargsMetricsRecorderProtocol + zero-overheadNullMetricsRecorderdefault + optional OpenTelemetry adapter underjvspatial[otel]JsonDBTransactionstrict by default (raisesNotImplementedError); adds opt-inbest_effort=Truebuffered mode marked@experimentalDatabase.supports_transactionscapability flagDeferredSaveMixin.max_pending_savesauto-flush safety netQueryEngineoptimization cache with LRU@experimentaland@deprecateddecorators with once-per-process semanticspytest-benchmarkregression suite (13 benches) + CI workflow with PR-comment comparisonCONTRIBUTING.md,RELEASING.md,CODE_OF_CONDUCT.md,docs/md/{stability,observability,benchmarks}.md,py.typedpip-auditworkflow; raises CI coverage gate to 60%Description
Bug Fixes
1. JsonDB orphan-tmp sweep race condition (uncovered during Phase A4 development)
bulk_savecalls would intermittently fail withENOENTon rename. The first writer to call_get_collection_dir()triggered anrglob("*.jvtmp")sweep that unlinked in-flight temp files of other concurrent writers.threading.Lock(_init_lock) gating the sweep so it runs exactly once before any concurrent write can proceed. Fast-path check outside the lock keeps steady-state cost at one branch.2. Silent-no-op
JsonDBTransactionJsonDBTransaction(db).save/get/delete/find()silently delegated to the underlying database without any transactional guarantee, thencommit()returned successfully. Callers thought writes were transactional; they weren't.NotImplementedErrorwith a clear message pointing toDatabase.supports_transactionsor thebest_effort=Trueopt-in. The opt-in mode buffers writes/deletes in memory and applies on commit (with documented semantics — atomic against single-process readers, not against process crashes mid-commit). Marked@experimentalso adopters see a once-per-process warning.Feature Request
Phase A1 — IO durability
jvspatial/db/_atomic.py:atomic_write_bytes/atomic_write_text(temp + fsync + rename + dir-fsync). Adopters get crash safety on JsonDB and LocalFileInterface for free. Skips dir-fsync under serverless mode where the FS is tmpfs.jvspatial/db/_path_locks.py:PathLockManager— bounded LRU per-path locks. Concurrent writes to different files run in parallel; same-file writes serialize. Cross-thread safe (still works fromasyncio.run-in-side-thread callers likeDBLogHandler).Phase A2 — Native count + filter pushdown
Database.count()is now native on every backend.SELECT COUNT(*)plus a Mongo→SQL query translator (jvspatial/db/_sqlite_translate.py) supporting$eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$exists, top-level AND, and$and/$or(recursive) intojson_extract()WHERE clauses withLIMIT/ORDER BYpushdown. Falls back to legacy in-Python filter for$regex/$elemMatch/etc. Field paths validated against a strict regex; values are bound parameters — no SQL-injection surface.count_documents/estimated_document_count.Select="COUNT"via Scan or GSI Query.Object.count()andObject.find()benefit transparently.Phase A3 — Caching
CachingDatabasewrapper (jvspatial/db/_cache.py): opt-in LRU+TTLget-cache, negative caching, save/delete/find-one-and-* invalidation. Skipped under serverless mode. Wired viacreate_database(cache_get_size=, cache_get_ttl=).QueryEngineoptimization cache with an LRU (default 1024, configurable, set to 0 to disable).Phase A4 — Bulk APIs
Database.find_many(ids)andDatabase.bulk_save(records)added to the protocol. Native overrides:find({"_id": {"$in": ids}})/bulk_writewithordered=False.WHERE id IN (...)SELECT (500-id chunks) / single-transactionexecutemany(all-or-nothing).BatchGetItem/BatchWriteItem.asyncio.gather/ parallel atomic per-file writes.CachingDatabasesplitsfind_manybetween cache hits and a single backend call for misses.DeferredSaveMixingainsmax_pending_savesclass var (defaultNone/disabled) — a safety net that triggers auto-flush if N deferred saves accumulate without an explicit flush.Phase A5 — Resilience
jvspatial/utils/retry.py: shared asyncretry_async()and@retry()decorator. Exponential backoff + full jitter (per AWS Architecture Blog's analysis), configurable retryable predicate (exception class, tuple, or callable),on_retryhook for log/metric integration.save/get/deleteauto-retry onProvisionedThroughputExceededException,ThrottlingException,RequestLimitExceeded,TooManyRequestsException,TransactionConflictException.save_file/get_file/delete_fileretry onSlowDown,RequestTimeout,ServiceUnavailable, 5xx.JVSPATIAL_S3_MULTIPART_THRESHOLDenv). Uses boto3'sTransferManagerfor splitting + parallel parts + resume-on-failure.Phase B1/B2 — Observability
MetricsRecorderProtocol (jvspatial/observability/metrics.py):record_duration/increment_counter/record_valuewith**labels. Runtime-checkable.NullMetricsRecorderdefault (zero overhead).OpenTelemetryMetricsRecorder(jvspatial/observability/otel.py) underpip install jvspatial[otel]. Targets the OTel meter API; if no SDK is configured, calls become no-ops by design.ObservableDatabasewrapper (jvspatial/db/_observable.py) emits a structured log line per DB op withbackend/op/collection/duration_ms/success/result_count. WARNING-elevation at theslow_query_msthreshold (default 100ms). Plus four metrics:jvspatial.db.op.{duration_seconds, count, slow_count, result_count}.Phase B3 — Benchmarks
tests/benchmarks/: 13 benches across JsonDB / SQLite / DeferredSaveMixin guarding the IO wins..github/workflows/benchmarks.ymlruns on PRs, compares against the latestbench-baselineartifact (published frommain), posts a markdown comparison comment. Hard-gating off (CI variance too high); informational warning at >25% regression.Refactor Request
ConnectionFailure+ "Event loop is closed"RuntimeErrorwere duplicated 8 ways. Refactoredsave/get/delete/findto share a_run_with_reconnect(coro_factory)helper that uses the newretry_asyncwith anon_retryhook that resets_client/_db. Behavior preserved exactly (one retry on transient connection error with state reset).asyncio.Lockserializes writes); added a docstring block explaining the trade-off and the single-event-loop assumption rather than building a pool that wasn't needed.QueryEngine.optimize_querycache changed from unboundeddicttoOrderedDict-backed LRU. Newcache_evictionscounter in_optimization_stats.Changes Made
High-Level Summary
_atomic.py,_path_locks.py) wired into JsonDB, LocalFileInterfaceJsonDBTransactionstrict by default, opt-in best-effort marked experimental,Database.supports_transactionscapability flagcount()everywhere + Mongo→SQL query translator for SQLite (_sqlite_translate.py)find_many,bulk_save) with native overrides on all four backendsCachingDatabase(LRU+TTL, negative-cache, invalidation, serverless-skipped)ObservableDatabase+MetricsRecorderProtocol + Null default + optional OTel adaptermax_pending_savesauto-flush safety net@experimental+@deprecateddecorators with once-per-process semanticsCONTRIBUTING.md,RELEASING.md,CODE_OF_CONDUCT.md,docs/md/{stability,observability,benchmarks}.md,py.typedPEP-561 markerpip-auditworkflow, coverage gate raised 55 → 60%threading.Lockto stop racing concurrent writersQueryEngineoptimization cache (LRU, default 1024)File counts: 62 files changed, +7,340 / -306. Breakdown: 4 new internal modules (
_atomic,_path_locks,_cache,_observable,_sqlite_translate), 3 new utility modules (stability,deprecation,retry), 3 new observability modules (__init__,metrics,otel), 12 new test files (~110 cases), 4 new doc pages, 4 new community files, 2 new CI workflows.Checklist
Mark all that apply:
docs/md/{observability,benchmarks,stability}.mdnew;CONTRIBUTING.md,RELEASING.md,CODE_OF_CONDUCT.mdnew at repo root;README.mdcross-links;CHANGELOG.md[Unreleased]block).pytest tests/benchmarks --benchmark-only).pytest-benchmarkadded todev+testextras for the regression suite;opentelemetry-apiadded as a new optional[otel]extra — not pulled into core).Steps to Test
pytest tests/db/test_atomic.py \ tests/db/test_path_locks.py \ tests/db/test_transaction_semantics.py \ tests/db/test_query_cache_lru.py \ tests/db/test_caching_database.py \ tests/db/test_observable_database.py \ tests/db/test_bulk_apis.py \ tests/db/test_jsondb_count.py \ tests/db/test_sqlite_translate.py \ tests/db/test_sqlite_pushdown.py \ tests/storage/test_local_version_atomicity.py \ tests/storage/test_s3_multipart.py \ tests/utils/test_retry.py \ tests/utils/test_deprecation.py \ tests/utils/test_stability.py \ tests/observability/test_metrics_recorder.py \ tests/observability/test_otel_adapter.py \ tests/core/test_deferred_save_auto_flush.py -vExpect a structured log line per op via the
jvspatial.db.observablelogger.Confirm downstream compatibility —
jvagentandintegraltest suites should pass against the new commit without any source change. (Audit during this PR confirmed zero callers of the only meaningfully-breaking surface,JsonDBTransactiondirect usage.)Regression check — run the benchmark suite before and after, confirm no >25% slowdown on any of the 13 benches.
Additional Context
Stability tiers.
docs/md/stability.mddeclares which APIs are public/stable, internal, or experimental — and the deprecation policy. New public surface:Database.find_many,Database.bulk_save,Database.supports_transactions,MetricsRecorderProtocol,NullMetricsRecorder,OpenTelemetryMetricsRecorder. New experimental surface:JsonDBTransaction(best_effort=True)(emitsExperimentalWarningon first use per process). Internal-only:_atomic,_path_locks,_sqlite_translate,_cache,_observable— used viacreate_database()factory kwargs.Serverless safety. Every new layer that has a "long-lived process" assumption (cache, auto-flush, orphan-tmp sweep, dir-fsync, once-per-process warnings) checks
is_serverless_mode()and degrades cleanly. Same pattern as the existingDeferredSaveMixin.Performance impact (rough):
User.count()on a 100K-row SQLite table: was O(100K) JSON parse + Python filter; now O(1) server-side.User.find({"active": True})on the same: was full-table load + filter; now WHERE-pushdown.count()without GSI on a 100K-item table: was 100K item bodies pulled across the wire; nowSelect=COUNTreturns just the count.count(): was N reads + N JSON parses; now N dirent entries.put_object; now ~25 parallel multipart uploaders with resume-on-failure.Breaking change inventory (all surfaces):
JsonDBTransaction(db).save/get/delete/find()raisesNotImplementedErrorby default (was silent no-op). Migration: passbest_effort=Truefor the buffered semantics, OR checkDatabase.supports_transactionsand fall back to non-transactional writes.That is the only meaningfully-breaking change. Audited downstream consumers (
jvagent,integral) — neither uses this surface, so no coordinated change required.Questions or Concerns
Should
[otel]go into the default install? Currently it's a separate extra (pip install jvspatial[otel]). Keeps core dep tree small. If you'd rather have OTel ride along by default, easy switch — move it intodependencies.CI coverage gate at 60%. Bumped from 55%. Could go higher given the new tested code lands but I'd rather measure first to avoid CI flake. Open to tightening to 65–70% in a follow-up.
Mongo
supports_transactions = Trueis the adapter claim, not a deployment guarantee (still requires a replica set). I documented this in the docstring; should it surface more loudly somewhere — e.g. a runtime check inbegin_transaction()that raises a clearer error than the underlyingpymongoone?Per-process cache under autoscaling.
CachingDatabaseis local to each worker. For a service running 8 replicas behind a load balancer, a write to one worker doesn't invalidate the others' cached copies until TTL expires. Documented indocs/md/observability.mdbut worth a louder warning if anyone tries it in production. Open to adding a "use Redis-backed cache instead" pointer in the constructor docstring.OpenTelemetry tracing not included in this PR. Only metrics. A
TracerProtocol parallel toMetricsRecorderwould be a natural follow-up; happy to do it as a separate PR if you'd rather not stack it on this one.