Skip to content

Develop#11

Merged
jeffersonrodrigues92 merged 6 commits into
mainfrom
develop
May 25, 2026
Merged

Develop#11
jeffersonrodrigues92 merged 6 commits into
mainfrom
develop

Conversation

@jeffersonrodrigues92

Copy link
Copy Markdown
Contributor

No description provided.

jeffersonrodrigues92 and others added 6 commits May 21, 2026 06:58
Drop the URL :tenantId path segment and adopt the Lerian database-per-tenant
model. Tenant scope is resolved from context.Context via the lib-commons
tenant-manager, so admin routes carry no tenant identifier and storage rows
hold no tenant_id column. Each tenant has its own database; isolation is
natural by connection routing.

Storage layer
- systemplane_entries (Postgres): PK (namespace, key); trigger payload is
  {namespace, key, op}. No tenant_id column.
- MongoDB compound _id is {namespace, key}. No tenantID field.
- New store.Entry/Event types with Op = upsert|delete.

Connection resolution
- WithMultiTenantEnabled() toggle; WithModule(name) defaults to "systemplane".
- Multi-tenant: resolveDB(ctx) / resolveCollection(ctx) call
  tmcore.GetPGContext / tmcore.GetMBContext. Lazy CREATE TABLE IF NOT EXISTS
  per tenant DB via sync.Map cache.
- Single-tenant: static *sql.DB / *mongo.Client from the constructor
  (unchanged behavior).

LISTEN/NOTIFY and in-process cache
- Single-tenant: kept (LISTEN connection for Postgres, change stream for
  Mongo, in-process value cache).
- Multi-tenant: disabled. Every Get hits the resolved tenant DB; OnChange
  and Subscribe return ErrNotSupportedInMultiTenant.

Client surface (collapsed)
- Set/Get/Delete/List/Register are ctx-aware; *ForTenant variants gone.
- OnChange callback signature: func(ctx, ns, key, newValue).
- IsRegistered replaces KeyStatus.

Admin
- Four routes: GET/PUT/DELETE /:ns/:key and GET /:ns. The :tenants and
  :tenantID segments are gone. WithAuthorizer hooks read|write; the
  authorizer may inspect c.UserContext() for tenant-aware policy.

Errors
- New: ErrNotSupportedInMultiTenant, ErrTenantConnectionMissing.
- Dropped: ErrMissingTenantContext, ErrInvalidTenantID,
  ErrTenantScopeNotRegistered, ErrTenantSchemaNotEnabled.

Tests
- Unit: 38 (-race). Contract suite collapsed and parameterized with
  RunOptions{SkipSubscribe}.
- Integration: Postgres + Mongo via testcontainers, two databases inside
  one container exercise multi-tenant isolation and schema-once caching.

Docs
- README and CLAUDE.md rewritten for the two operating modes.
- MIGRATION_TENANT_SCOPED.md deleted.

X-Lerian-Ref: 0x1
Apply the 15 items raised by CodeRabbit on PR #9.

Real bugs (correctness)
- mongodb: key schema cache by stable string "<db.Name()>/<collection>"
  instead of the *mongo.Collection pointer, which is not stable across
  resolveCollection calls in mongo-driver v2.
- mongodb: use db.CreateCollection in multi-tenant bootstrap so the
  collection actually exists before the first Get. Single-tenant keeps
  Indexes().List to preserve the existing change-stream attach semantics
  (CreateCollection caused a flaky first-insert race against the oplog
  watcher in single-tenant integration tests). The asymmetry is documented
  in runSchema.
- mongodb polling: emit OpDelete by diffing snapshotKeys() between polls.
- mongodb polling: dedupe at watermark boundary via seenAtWatermark set
  with >= filter, so two writes inside the same millisecond are both
  observed.
- mongodb changestream: Subscribe spawns a goroutine that observes
  ctx.Done() and runs the shared unsubscribe — canceled callers no longer
  leak handlers.
- mongodb: List passes options.Find().SetSort on (namespace, key) so the
  docstring's ordering guarantee holds.
- client: typed accessors (GetString/Int/Bool/Float64/Duration) return
  (zero, false, ErrValidation) on type/parse failure instead of silently
  collapsing to zero values. GetInt rejects fractional float64.
- client: Start re-checks closed under startMu; Close acquires startMu
  inside closeOnce.Do so Start and Close are mutually exclusive.
- client: hydration vs changefeed race resolved by tracking
  hydrationTouched during Start and skipping those keys in hydrate, so a
  changefeed event that lands during hydration is not overwritten by the
  stale List snapshot.

Medium hardening
- mongodb + postgres: empty namespace/key wrap store.ErrValidation (newly
  added); client.ErrValidation is aliased so the admin layer's 400
  mapping continues to work end-to-end.
- mongodb + postgres: drop the high-cardinality actor attribute from
  Delete spans. Actor still flows through UpdatedBy on the row.

Refactor
- client: Client owns a lifecycleCtx canceled by Close; fireSubscribers
  threads it through to OnChange callbacks instead of context.Background.
- admin tests: setupClient returns (*Client, *fakeStore) so write-path
  assertions can verify the actor extractor reached Delete.
- admin tests: namespace filtering test seeds a second namespace and
  asserts it is excluded.

Nit
- mongodb integration tests: the missing-tenant-context case no longer
  starts a container — the assertion is purely in-process.

Tests: 46 unit (with -race, +8 new) and 31 integration pass.
X-Lerian-Ref: 0x1
CodeRabbit fixes (7)
- client: hydrationTouched is now set AFTER the refresh succeeds, so a
  failing changefeed refresh no longer poisons the cache by skipping the
  later hydrate List snapshot.
- client: kept the //nolint:gosec on the lifecycle context.WithCancel
  allocation — removing it (as CodeRabbit suggested) re-fires G118 under
  this repo's gosec config; the directive is load-bearing because cancel
  is owned by Close() rather than deferred locally. Restored with a
  clearer rationale comment.
- mongodb Subscribe: nil-ctx guard and a single sync.Once that wraps both
  subscriber-map removal AND cancelCh close, so concurrent ctx.Done() and
  caller-driven unsubscribe can no longer double-close the channel.
- mongodb ensureSchema: failure no longer pinned. On runSchema error the
  cached *sync.Once and the schemaErr entry are both evicted so the next
  call retries. Added a schemaRunner test seam to exercise this without
  a live MongoDB.
- client Close: store.Close() error is now captured and returned wrapped
  ("systemplane: close store: ...") instead of being discarded.
- client Get/List: json.Unmarshal failures on stored values log at error
  level and return a wrapped error instead of falling back to the
  default value with ok=true; corrupted JSON is no longer indistinguishable
  from a missing row.
- admin: TestAdmin_Delete now asserts that the actor produced by
  WithActorExtractor reaches the store (LastDeleteActor accessor on the
  fakeStore).

goleak coverage
- TestMain with goleak.VerifyTestMain added to internal/client,
  internal/mongodb, internal/postgres. Ignore lists are conservative and
  documented (testcontainers Reaper, mongo-driver topology shutdown,
  pgxpool.backgroundHealthCheck owned by the caller's DB, net/http
  persistConn keep-alive on the Docker client).
- Targeted assertions covering: Subscribe ctx-observer exit on cancel,
  Subscribe explicit unsubscribe, concurrent double-unsubscribe + ctx
  cancel race (idempotent under sync.Once), nil-ctx safety, change
  stream watcher cleanup on Close, polling ticker cleanup, postgres
  LISTEN reader cleanup on Close.
- go.uber.org/goleak promoted from indirect to direct dependency.

Tests: 56 unit (+9) and 20 integration (+4) pass with -race; lint, vet,
gofmt, go mod tidy/verify all clean.

X-Lerian-Ref: 0x1
…a callers

The failure branch of the once.Do closure in ensureSchemaByKey stored the
bootstrap error and immediately deleted it. Concurrent callers that joined
the same once.Do — whose local runErr was nil because they did not execute
the closure — would then read schemaErr.Load after the delete and observe
nil, masking the bootstrap failure as a successful no-op.

Remove the immediate Delete in the failure path. Keep schemaOnce.Delete so
future attempts retry, and keep the success branch's schemaErr.Delete so
the tombstone is cleared on the next successful bootstrap (the
success-branch Delete is now load-bearing rather than defensive).

Update the existing TestEnsureSchema_TransientFailureRetries assertion that
expected immediate eviction; it now asserts the tombstone remains after a
failed run and is cleared after the subsequent success.

Add TestEnsureSchema_ConcurrentCallersObserveFailure to pin the race: 64
worker goroutines join the same once.Do with a runner that fails behind a
release channel, and every worker MUST observe the masked error. Verified
regression-sharp by stashing only the production fix: 63 of 64 workers
silently dropped the failure; with the fix all 64 observe it.

X-Lerian-Ref: 0x1
The polling-mode boundary dedupe keyed seenAtWatermark by (namespace, key)
only. Two writes to the same row landing in the same BSON millisecond
caused the second write to be silently skipped: the first poll recorded
the key, and the next poll's $gte query returned the row again and matched
the dedupe set without comparing the actual value — so the new value never
reached subscribers and peer caches stayed stale until some later write
advanced the watermark.

Carry a content discriminator in the dedupe set: each entry is now a
seenEntry{valueHash} where valueHash is FNV-64a over the raw entryDoc.Value
bytes (the JSON payload is already stored as a BSON string in the document,
so no marshal round-trip). The dedupe rule becomes "skip only when (key,
hash) match"; idempotent rewrites still dedupe, real rewrites now emit.

Tests:
- TestBoundaryDedupHit_DecisionRule pins all five branches of the rule
- TestHashValue_Stability pins hashValue determinism and discrimination
- TestIntegration_PollOnce_SameMsDifferentValue_EmitsBoth is a regression
  guard against the original bug (pre-fix emits one event, post-fix emits
  two)
- TestIntegration_PollOnce_SameMsSameValue_EmitsOnce is the negative case
  for idempotent rewrites

CodeRabbit also flagged //nolint:gosec on the lifecycle context.WithCancel
allocation (client.go:153) as stale. Verified by removing the directive
and running golangci-lint: gosec G118 fires immediately because cancel is
released from Close() rather than via a deferred call. Directive is
load-bearing; kept with the existing inline rationale.

X-Lerian-Ref: 0x1
feat: database-per-tenant compliance (closes #6)
@jeffersonrodrigues92
jeffersonrodrigues92 merged commit b2a282d into main May 25, 2026
6 of 8 checks passed
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73a6f9c5-f053-4b76-9783-a6e7e17fc244

📥 Commits

Reviewing files that changed from the base of the PR and between ca23e25 and b238da5.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (86)
  • CLAUDE.md
  • README.md
  • admin/admin.go
  • admin/admin_fuzz_test.go
  • admin/admin_global_extra_test.go
  • admin/admin_response_extra_test.go
  • admin/admin_responses.go
  • admin/admin_tenant.go
  • admin/admin_tenant_extra_test.go
  • admin/admin_tenant_test.go
  • admin/admin_test.go
  • api_client.go
  • api_compat_test.go
  • api_constructors.go
  • api_errors.go
  • api_facade_test.go
  • api_testing.go
  • go.mod
  • internal/client/bench_tenant_test.go
  • internal/client/client.go
  • internal/client/client_accessors_lifecycle_test.go
  • internal/client/client_lifecycle_error_test.go
  • internal/client/client_startup_events.go
  • internal/client/client_telemetry.go
  • internal/client/client_test.go
  • internal/client/client_testing.go
  • internal/client/client_testing_test.go
  • internal/client/doc.go
  • internal/client/errors.go
  • internal/client/get.go
  • internal/client/main_test.go
  • internal/client/metrics.go
  • internal/client/onchange.go
  • internal/client/options.go
  • internal/client/perf_gate_test.go
  • internal/client/refresh.go
  • internal/client/register.go
  • internal/client/set.go
  • internal/client/tenant_cache.go
  • internal/client/tenant_cache_lru.go
  • internal/client/tenant_hydration.go
  • internal/client/tenant_lazy.go
  • internal/client/tenant_list.go
  • internal/client/tenant_onchange.go
  • internal/client/tenant_onchange_test.go
  • internal/client/tenant_race_test.go
  • internal/client/tenant_scoped.go
  • internal/client/tenant_scoped_accessors.go
  • internal/client/tenant_scoped_accessors_test.go
  • internal/client/tenant_scoped_lazy_test.go
  • internal/client/tenant_scoped_register_test.go
  • internal/client/tenant_scoped_smoke_test.go
  • internal/client/tenant_scoped_test.go
  • internal/client/tenant_value_clone_test.go
  • internal/client/value_clone_test.go
  • internal/mongodb/fields.go
  • internal/mongodb/main_test.go
  • internal/mongodb/mongodb.go
  • internal/mongodb/mongodb_changestream.go
  • internal/mongodb/mongodb_changestream_test.go
  • internal/mongodb/mongodb_crud.go
  • internal/mongodb/mongodb_fuzz_test.go
  • internal/mongodb/mongodb_goleak_integration_test.go
  • internal/mongodb/mongodb_integration_test.go
  • internal/mongodb/mongodb_migration.go
  • internal/mongodb/mongodb_migration_integration_test.go
  • internal/mongodb/mongodb_migration_legacy.go
  • internal/mongodb/mongodb_poll.go
  • internal/mongodb/mongodb_polling_integration_test.go
  • internal/mongodb/mongodb_tenant.go
  • internal/mongodb/mongodb_tenant_integration_test.go
  • internal/mongodb/mongodb_test.go
  • internal/postgres/main_test.go
  • internal/postgres/postgres.go
  • internal/postgres/postgres_fuzz_test.go
  • internal/postgres/postgres_goleak_integration_test.go
  • internal/postgres/postgres_integration_test.go
  • internal/postgres/postgres_listen.go
  • internal/postgres/postgres_listen_test.go
  • internal/postgres/postgres_migration_integration_test.go
  • internal/postgres/postgres_schema.go
  • internal/postgres/postgres_tenant.go
  • internal/postgres/postgres_tenant_integration_test.go
  • internal/postgres/postgres_test.go
  • internal/store/store.go
  • systemplanetest/contract.go

Walkthrough

This PR comprehensively refactors the systemplane library to remove tenant-scoped public APIs, restructure multi-tenant support, and make read accessors context-aware with explicit error returns. It shifts from a complex dual-registry design to a two-mode architecture (single-tenant process-wide cache with changefeed, multi-tenant context-driven per-request resolution) and eliminates tenant-aware HTTP admin routes in favor of a simpler four-route design.

Changes

Public API and implementation refactoring

Layer / File(s) Summary
Public API surface refactoring
api_client.go, api_constructors.go, api_errors.go, api_testing.go
Remove RegisterTenantScoped and all tenant-scoped read/write/subscription methods; add context-aware getters returning (value, ok, error) for Get, GetString, GetInt, GetBool, GetFloat64, GetDuration; update List to return ([]ListEntry, error) and OnChange to return (func(), error) with context-aware callback; add IsRegistered(ns, key) bool; introduce WithMultiTenantEnabled() and WithModule(name) constructor options; export new multi-tenant sentinel errors ErrNotSupportedInMultiTenant, ErrTenantConnectionMissing.
Admin HTTP routes simplification
admin/admin.go
Eliminate tenant-scoped routes (PUT/DELETE/GET tenant overrides); register only four namespace/key routes (GET list, GET read, PUT write, DELETE delete); simplify mountConfig to single authorizer plus actor extractor; update all handler implementations to call context-aware client methods with c.UserContext(); refactor error handling and authorizer invocation.
Admin handler tests rewrite
admin/admin_test.go
Replace multi-feature test infrastructure with lightweight fakeStore supporting only global CRUD; add eight focused tests for GET/PUT/DELETE/LIST, unknown-key rejection, 404 handling, delete actor propagation, list filtering, default-deny authorization, custom prefix routing.
Client struct and lifecycle refactoring
internal/client/client.go
Remove tenant-scoped registry/cache/subscribers, singleflight machinery, metrics, and goroutine lifecycle fields; add multiTenant bool, simplified registry/cache/subscriber maps, hydration tracking (hydrating, hydrationTouched), and lifecycle context/cancelation; rewrite newClient and Client.Start to seed cache from defaults, coordinate hydration vs changefeed updates; refactor Client.Close to cancel lifecycle context, unsubscribe, close debouncer; update refresh/dispatch to use lifecycle context.
Context-aware read and write accessors
internal/client/get.go, internal/client/set.go
Update Get and typed getters to accept context.Context, return (value, ok, error) with strict type validation surfacing ErrValidation on mismatch; refactor List to be context-aware with deterministic ordering, branching to listFromStore (multi-tenant) or listFromCache (single-tenant); add JSON decode error handling; update Set to use UTC timestamps and single-tenant-only cache write-through; add new Delete method.
Client registration and subscription helpers
internal/client/register.go, internal/client/onchange.go, internal/client/options.go
Simplify Register to inline namespace/key validation; add IsRegistered(ns, key) bool helper; refactor OnChange to return (func(), error), accept context-aware callback, reject multi-tenant with ErrNotSupportedInMultiTenant; trim clientConfig and add WithMultiTenantEnabled(), WithModule(name) options; remove tenant-schema, resume-token, postgres-isolation options.
Telemetry simplification
internal/client/client_telemetry.go
Remove OpenTelemetry span/tracing plumbing (span types, attribute helpers, startSpan); retain logger-level helpers; expand nil-guards to check both nil receiver and nil logger; add logError helper.
Client testing infrastructure update
internal/client/client_testing.go, internal/client/client_test.go
Simplify TestStore interface by removing tenant methods, add TestEvent.Op field, remove TestEntry.TenantID; update testStoreAdapter to omit tenant tracking; create new memStore in-memory test double supporting both single/multi-tenant modes with listHook for hydration timing control; add 30+ focused tests validating registration lifecycle, cache/store synchronization, multi-tenant read-through, typed accessor validation, race safety, JSON corruption handling, hydration ordering.
Client errors and package documentation
internal/client/errors.go, internal/client/doc.go, internal/client/main_test.go
Change ErrValidation to alias store.ErrValidation; remove tenant-scoped sentinel errors; add ErrNotSupportedInMultiTenant, ErrTenantConnectionMissing; expand doc.go to describe two operating modes; add TestMain with goleak.VerifyTestMain.
MongoDB store two-mode refactoring
internal/mongodb/mongodb.go
Update Config to add MultiTenantEnabled, Module fields and remove schema/resume-token options; change document _id from tenant-aware to compound {namespace, key}; add lazy per-collection schema bootstrap caches (schemaOnce, schemaErr); implement new Start(ctx) method; add resolveCollection(ctx) for multi-tenant database routing; update all CRUD methods to use compound _id and multi-tenant collection resolution.
MongoDB change-stream and polling refactoring
internal/mongodb/mongodb_changestream.go
Replace per-subscription streaming with shared listener multiplexing events; implement new Subscribe returning (unsubscribe, error) and rejecting multi-tenant with ErrNotSupportedInMultiTenant; add polling path with content-hash dedup; simplify change-stream path with compound _id decoding; implement subscriber snapshot dispatch with panic recovery.
MongoDB CRUD and schema bootstrap
internal/mongodb/mongodb_crud.go, internal/mongodb/fields.go, internal/mongodb/main_test.go
Add runSchema with multi-tenant eager vs single-tenant index validation logic; add isNamespaceExists helper; replace tenant/phase-aware upsert with collection-parameterized primitive using compound _id; reorganize field constants; add goleak.VerifyTestMain with third-party goroutine ignores.
MongoDB test infrastructure
internal/mongodb/mongodb_changestream_test.go, internal/mongodb/mongodb_goleak_integration_test.go
Add unit tests for Subscribe teardown on context cancel/explicit unsubscribe/concurrent unsubscribe/racing cancel-vs-unsubscribe; add schema bootstrap concurrency/eviction tests; add polling dedup helper tests; add integration tests for change-stream and polling cleanup on Close().
Root package test cleanup
api_compat_test.go, api_facade_test.go, go.mod
Remove api_compat_test.go (no longer needed). Remove api_facade_test.go (simplified test surface). Update go.mod to make fiber, dbresolver, goleak direct deps; move testify, sync to indirect.
Documentation and guides update
CLAUDE.md, README.md
Replace CLAUDE.md "Runtime configuration" with "Operating modes" and "API invariants" documenting single-tenant vs multi-tenant behavior, storage schema, root package API, sentinel errors. Update README.md: clarify operating modes, update Quickstart examples to use context-aware getters with error handling, add MongoDB polling configuration, rewrite multi-tenant examples with tenant-manager Fiber middleware wiring, simplify admin routes documentation, remove "Operational safety options" and "Tenant-scoped overrides" sections.
Admin response error mapping
admin/admin_responses.go
Add new multi-tenant sentinel error mappings; remove old tenant-scoped error mappings; update default error message text.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant