Skip to content

Commit ee87139

Browse files
feat(pkg): natsrouter improvements + mongoutil bulk API + new minioutil (#157)
* feat(natsrouter): admission control + panic safety + graceful shutdown + HandlerTimeout Five improvements to pkg/natsrouter, designed and reviewed via docs/superpowers/plans/2026-04-30-natsrouter-improvements.md: - WithMaxConcurrency option + ErrUnavailable error: bounds in-flight handler goroutines with a configurable cap (default unbounded / gin-style; set to a positive int to throttle). Excess requests get a fast ErrUnavailable reply rather than blocking the consumer. - Spawn-site panic backstop: a recover() wrapper around the spawned handler goroutine releases the semaphore slot and decrements the in-flight WaitGroup so a panicking handler can't deadlock the router or leak permits. The backstop also publishes a "internal error" reply so the requester sees a response rather than a timeout. - Shutdown waits for in-flight handlers: r.Shutdown(ctx) drains the in-flight WaitGroup before returning, with the supplied ctx bounding the wait. Prevents the long-standing race where nc.Drain() returns before handler goroutines have finished writing their replies. - HandlerTimeout middleware: per-handler deadline that returns ErrTimeout when the inner handler runs past its budget. Composes cleanly with the spawn-site backstop. - Default() constructor + doc.go example + README rewrite: newcomers can now read pkg/natsrouter/README.md end-to-end and build a working service without bouncing through the older spec docs. Reviewed across multiple rounds by bug, architecture, and NATS-expert agents; consolidated errata applied. Integration tests exercise the admission cap, the panic backstop (process survives + follow-up requests succeed), and the shutdown wait. * fix(search-service): close shutdown gap; rename Header to GetHeader Two small fixes to search-service/main.go surfaced during the natsrouter shutdown work: - Shutdown now waits for the natsrouter's in-flight handlers before draining nc, closing the previous race where requests arriving during shutdown could be dropped after the consumer drained but before handler goroutines finished. - Rename Header() -> GetHeader() to match the *otelnats.Conn idiom the rest of the codebase uses. * feat(mongoutil): promote generic helpers + add three-layer bulk-write API Two changes that share the same package boundary: 1. PROMOTION (refactor). Move the generic Mongo helpers out of history-service/internal/mongorepo/ into the shared pkg/mongoutil: - Collection[T] generic wrapper (FindOne / FindByID / FindMany with (nil, nil) not-found semantics; Aggregate; AggregatePaged with $facet pagination + 16 MB caveat documented; Raw escape hatch). - OffsetPageRequest / OffsetPage[T] / EmptyPage[T] / NewOffsetPageRequest pagination types. - QueryOption + WithProjection / WithSort / WithLimit / WithSkip functional options. history-service/internal/mongorepo retains domain-specific repos (subscription, threadroom, pipelines) and flips imports to mongoutil. The TestCollection_* integration tests extract from subscription_test.go into pkg/mongoutil/collection_integration_test.go; subscription_test.go keeps only TestSubscriptionRepo_*. New mongorepo/setup_test.go is a thin testutil.MongoDB wrapper used by the remaining domain tests. Mock file regenerated to use the new imports. No logic changes to the domain code -- pure relocation plus reference flips. 2. BULK-WRITE API (new). Three layers mirroring FindOne/FindByID: - BulkWrite (foundation): wraps mongo-driver's BulkWrite with explicit SetOrdered(false), short-circuits on empty input to (nil, nil), preserves the partial-success *BulkResult on partial failure so callers can inspect WriteErrors via errors.As(err, &mongo.BulkWriteException{}). - BulkUpsert (typed convenience): builds N UpsertModels from items + filter mapper. \$set MERGE semantics (preserves stored fields not in T). bsonSetWithoutID strips _id from the marshal payload because MongoDB rejects updates to the immutable _id; _id is set on insert from the upsert filter. - BulkUpsertByID (ergonomic): pure pass-through with built-in bson.M{"_id": idFn(item)} filter. Cheapest possible bulk-upsert pattern (always-indexed _id, single B-tree lookup). - InsertMany (sibling to bulk-upsert): write-only batch with SetOrdered(false), returns (int64, error) so partial-failure callers see how many items got through. BulkResult mirrors mongo.BulkWriteResult fields the wrapper exposes; UpsertModel + DeleteModel are stateless write-model constructors; fromDriverResult is the driver-to-wrapper mapper. Empty-input contract is (nil, nil) for the bulk methods and (0, nil) for InsertMany; both are documented at the type level. Designed via docs/superpowers/specs/2026-05-06-mongoutil-extension- and-miniout-design.md (spec) and executed via the matching plan. Spec was reviewed across 2 rounds by bug, spec-consistency, senior architecture, and Mongo-expert agents before any code landed; each plan task was implemented via TDD and reviewed by spec-compliance plus code-quality agents (with Mongo expert on tasks involving substantive driver interaction). The "Post-merge amendments" section in the spec captures changes that landed after the resolution log was finalized (BulkUpsert _id strip, bsonSetWithoutID location + error wrapping, BulkUpsertByID always-indexed note). * feat(minioutil): typed JSON-blob wrapper around minio-go/v7 New pkg/minioutil follows the pkg/<provider>util convention used by cassutil/mongoutil/natsutil/valkeyutil. Surface: - Connect(ctx, endpoint, useSSL, accessKey, secretKey)(*minio.Client, error) Construct-only. Does NOT issue a connectivity probe -- a probe via ListBuckets requires s3:ListAllMyBuckets (account-wide IAM); real production deployments scope credentials to one bucket via s3:ListBucket on that bucket's ARN, so probing here would force broader IAM than the package needs. NewBucket carries the actual fail-fast probe (bucket-scoped BucketExists). The ctx parameter is retained for signature symmetry with valkeyutil.Connect. - Bucket[T any] + NewBucket[T](ctx, client, bucketName) (*Bucket[T], error) Typed wrapper binding a *minio.Client to a single bucket and a JSON-marshalable payload type T. NewBucket calls client.BucketExists at construction so a misconfigured MINIO_BUCKET env var fails the service at startup rather than failing every Get/Put silently. Does NOT create the bucket -- provisioning is owned by ops/IaC. - Bucket[T].Put(ctx, key, v) Marshals v as JSON, uploads with explicit Content-Type: application/json; charset=utf-8 so downstream tools (S3 CLI, browsers, other languages) can identify the payload format without out-of-band knowledge. - Bucket[T].Get(ctx, key) (*T, error) Returns (nil, nil) on missing key, matching Collection.FindOne's not-found semantics. The NoSuchKey response surfaces from obj.Stat() (minio-go's GetObject is lazy); errors.As against minio.ErrorResponse with Code=="NoSuchKey" is the discriminator. Two HTTP round trips per Get (HEAD via Stat, then GET via Decode); acceptable for the small-JSON-blob workload. - Bucket[T].List(ctx, prefix, maxKeys) ([]string, error) Lists object keys within the bucket (NOT bucket names). maxKeys=0 falls back to defaultListCap (1000 -- matches S3's per-page cap) to prevent accidental unbounded scans on misuse. context.WithCancel + defer cancel() is load-bearing: minio-go's ListObjects spawns a goroutine that fills the returned channel, and breaking out of the range loop without cancelling leaks that goroutine. MaxKeys is plumbed through to ListObjectsOptions so the server returns only min(maxKeys, 1000) per page rather than the full default. - Bucket[T].Delete(ctx, key) error Idempotent on non-versioned buckets -- relies on S3 native semantics (DELETE returns 204 regardless of prior existence). - Bucket[T].Raw() *minio.Client and Bucket[T].Name() string Escape hatches mirroring Collection[T].Raw() so callers needing features the wrapper does not surface (presigned URLs, multipart, conditional Put, tagging, versioning) can reach the underlying client without giving up the bucket binding. Test infrastructure: - pkg/testutil.MinIO(t, prefix) mirrors testutil.MongoDB: sync.Once- shared MinIO container across the test process plus a per-test bucket name derived from a stable fnv hash of t.Name(). Bucket names are S3-valid (lowercase letters, digits, hyphens; capped at 63 chars). Best-effort cleanup with a 30s timeout. - pkg/testutil/testimages adds the MinIO image pin so all minio-touching integration tests track the same version. - pkg/minioutil/minio_test.go has only the trivial Bucket[T].Raw / Name accessor unit test. Hand-rolled S3 stub-server unit tests were considered and rejected during round-2 review -- emulating S3's wire format is fragile against minio-go upgrades and provides false confidence; testcontainers MinIO is fast enough (~1m for the full suite) to be the sole behavioral backstop. - The List integration test uses goleak.IgnoreCurrent() snapshotted before the List call to verify the channel-cleanup pattern without false-positives from minio-go's HTTP keepalive goroutines (IdleConnTimeout=60s, longer than goleak's drain budget; goleak's defaults do not cover net/http.persistConn). New deps (all Apache-2.0): - github.com/minio/minio-go/v7 -- MinIO Go SDK - github.com/testcontainers/testcontainers-go/modules/minio -- test only - go.uber.org/goleak -- test only, for the List leak guard Designed via docs/superpowers/specs/2026-05-06-mongoutil-extension- and-miniout-design.md (the same design doc that covers the mongoutil extension; see prior commit). 7 implementation tasks (Tasks 9-15), each TDD + reviewed by spec-compliance, code-quality, and S3-expert agents on the load-bearing tasks (Connect, NewBucket, Get NoSuchKey, List goroutine cleanup). * fix(natsrouter,testutil): close Add+Wait race; tighten panic-backstop test fidelity; document best-effort cleanup CodeRabbit review on PR #157 surfaced three items on the squashed branch: (MAJOR) pkg/natsrouter/router.go — Shutdown's wg.Wait() phase could race with Add(1) calls from still-draining subscriptions when the closeLoop broke early on ctx.Done(). Per sync.WaitGroup docs, Add concurrent with Wait is undefined when the counter is zero (panic risk). Fix: track allClosed; only enter the wg.Wait block when every subscription confirmed close. If ctx expired before all subs closed, surface the error and let the caller's deadline take precedence -- remaining handler goroutines continue in the background until process exit. Comment block rewritten to explain the invariant. (NIT) pkg/natsrouter/integration_test.go — TestIntegration_SpawnSite- PanicBackstop used WithMaxConcurrency(2). With cap=2, a leaked semaphore slot would still leave capacity for the follow-up "ok" request, masking a cleanup regression. Switched to cap=1 so a leaked slot blocks the second request and the test actually observes slot release. Added a comment explaining why cap=1 is load-bearing. (NIT) pkg/testutil/minio.go — replaced two `_ = container.Terminate(ctx)` calls with explicit "best-effort cleanup" comments per CLAUDE.md "never ignore errors silently — comment if intentionally discarded". The discarded errors are intentional (init already failed; Docker reaps the container on test-process exit either way), now documented. * feat(model): expand Employee struct with full org hierarchy + Org/OrgType Replaces the 3-field Employee placeholder (AccountName, Name, EngName) with the full schema looked up from the employee MongoDB collection. New fields cover: - Identity: ID (_id), EmployeeID, EmployeeCategory, EmployeeRoom, Status, RosterCode, SiteID, Company. - Organisational hierarchy: 8 levels x 4 attrs each (description / id / name / tcName) for Department, Department1, Division, Division1, Section, Section1, Function, Function1; plus DivTCName, ManagedOrgIDs, Orgs []Org, and OrgCode (computed; bson:"-"). - Contact: Mail, Phone, Phones, JOSLocationURL, Location, LocationURL. - Shift: ShiftCode, ShiftCodeDescEn/Zh, ShiftStartTime, ShiftEndTime. - Supervisor: SupervisorAccountName, SupervisorID, SupervisorName, SupervisorEngName, SupervisorPhone, SupervisorPhones. Renames the Go field EngName -> EnglishName (bson tag stays "engName" so wire format is unchanged). Zero callers of model.Employee in the codebase today, so the rename has no breakage. Adds two supporting types: - OrgType (string enum: section1, section, department1, department, division1, division, function1, function). - Org (id / description / name / tcName / type). Every field has both bson and json tags per CLAUDE.md "All model structs get both json and bson tags". Empty/optional fields use omitempty consistently. * refactor(mongoutil,minioutil): trim verbose godocs to essentials Per project rule: max ~2 lines of comment, only when WHY is non-obvious. Cuts roughly 250 lines of doc that restated MinIO/Mongo SDK behavior, behavior already implied by function names + signatures, or multi-paragraph rationales better captured in commit history. What's kept: - One-line WHY notes for non-obvious behavior (e.g. (nil, nil) not-found contract, FindMany returns []T{} not nil for JSON, $set is MERGE not REPLACE, _id stripping is required because Mongo rejects updates to immutable _id, defer cancel() is load-bearing to avoid a goroutine leak). - One-line caveats with real footgun risk (omitempty caveat on BulkUpsert, 16 MB BSON limit on AggregatePaged $facet). - A brief explanation of why minioutil.Connect doesn't probe at startup (least-privilege IAM rationale). What's gone: - Multi-paragraph "Implementation note" sections that restated what the code clearly shows. - Field-by-field BulkResult godoc; replaced with a one-line summary + the empty-input contract on the type itself. - Long pitfall lists where 1 keyword would do. - "Standard wiring" code-example blocks in godoc. - Restatements of S3 / Mongo native behavior callers can find in upstream docs. Net: ~250 fewer comment lines across 5 files, no logic change. * fix: address CodeRabbit + apply max-2-line comment rule CodeRabbit (3 findings): - pkg/mongoutil/bulk.go: UpsertModel/DeleteModel return concrete *mongo.UpdateOneModel/*mongo.DeleteOneModel instead of the interface (Go idiom + CLAUDE.md "accept interfaces, return structs"). bulk_test.go drops the now-redundant type assertions. - pkg/natsrouter/integration_test.go panic-backstop test: typed payload struct instead of map[string]any (CLAUDE.md "never map[string]interface{} for NATS payloads"). - pkg/natsrouter/integration_test.go shutdown-wait test: client goroutines now collected via sync.WaitGroup; nc.Request errors drain through an error channel and are asserted, no more silent drops or tail goroutines past the assertion boundary. Comment trim (every modified file outside pkg/natsrouter): Max 2 lines per comment, mostly 1-liners. Cuts the BulkUpsert godoc from 5 lines to 2; InsertMany 3->1; minioutil.Connect 3->1; testutil.MinIO 11->2; search-service shutdown 5->1; plus several 2->1 collapses. Zero logic change. * feat(restyutil): typed Resty client wrapper for outbound HTTP New pkg/restyutil produces a *resty.Client with codebase defaults wired in: OTel transport (so trace context propagates on every outbound call), slog response logging at debug level, 30s timeout. Options compose on top: WithTimeout / WithHeader / WithBearerToken / WithRetries. Replaces the raw net/http boilerplate seen in pkg/oidc and pkg/searchengine — services adopting this can write client.R().SetContext(ctx).SetBody(req).SetResult(&resp).Post("/path") instead of building requests, checking statuses, and hand-rolling JSON decode at every call site. Adds github.com/go-resty/resty/v2 (Apache-2.0) as the canonical HTTP client per CLAUDE.md (the rule was previously aspirational -- nothing in the repo used Resty before this). Tests cover defaults, every Option, end-to-end round-trip via httptest, and retry-on-5xx behaviour. * fix(restyutil): apply 3-reviewer findings (drop WithRetries, add OnError + WithTransport, log hygiene) Three reviewers (bug / code-quality / senior-engineer) consolidated the same actionable items: - Drop WithRetries entirely. Resty's default retry condition only fires on transport errors, not 5xx, so the option was misleading without an accompanying retry-condition helper. YAGNI-applied: no current call site needs retries; bring it back when a real caller has a concrete retry policy in mind. - Rename WithRetries' max parameter -> moot (option dropped). The shadow of the Go 1.21 builtin `max` is no longer present. - Add OnError hook so transport errors (connect refused, DNS, ctx deadline, TLS) log too. Without it the helper REGRESSED observability vs. the raw net/http boilerplate it replaces. - Add WithTransport(http.RoundTripper) Option. Wraps the supplied transport with otelhttp so OTel propagation is preserved by construction, even with custom TLS / proxy / dialer. Required before pkg/oidc (which needs InsecureSkipVerify in dev) can migrate. - Strip query string from URL in slog log fields. URLs can carry ?token=... or ?api_key=... in the wild; CLAUDE.md "Never log tokens, passwords, or full message bodies". - Propagate request_id from ctx into the slog line via natsutil.RequestIDFromContext. CLAUDE.md "include in all log lines". Tests cover OnError firing on transport error, WithTransport preserving the custom round-tripper, request_id propagation, and query-string stripping. * fix(natsrouter,docs): close post-Shutdown dispatch race; sync stale plan docs CodeRabbit pass on the rebased branch surfaced 1 real bug + 5 cleanups. (MAJOR) router.go: Shutdown previously had a window where late NATS callbacks (subscription mid-drain or fired after ctx expiry) could still call admit() + wg.Add(1) + spawn a handler goroutine AFTER Shutdown returned. That goroutine could then race teardown of caller- owned dependencies (DB closed, NATS drained). Add a stopping atomic.Bool gate that Shutdown sets BEFORE any drain step; natsHandler checks it first and replies busy instead of admitting. Existing in-flight handlers still drain through the wg.Wait path; only NEW dispatches post-Shutdown-start are rejected. (NIT) router.go: removed the exported Registrar interface — used only by Register/RegisterNoBody/RegisterVoid in this package, no external mocks, no alternative implementation. Their first parameter is now *Router directly. Pure simplification: existing callers (which pass *Router today) keep working. CLAUDE.md "interfaces in the consumer". (NIT) restyutil_test.go: switched the two log-capture tests from slog.NewTextHandler to NewJSONHandler; CLAUDE.md "always JSON, never text-format". Updated the matched assertions. (DOC) 2026-04-30-natsrouter-improvements.md: marked the original "default 100" + per-route WithConcurrency wording as superseded with a post-implementation note. Shipped behavior: unbounded default, single router-level WithMaxConcurrency. (DOC) 2026-05-06-mongoutil-extension-and-minioutil.md Task 9: dropped the ListBuckets startup probe from the Connect code block; added a post-implementation note pointing at the spec's amendments section. Future agents executing this plan get the IAM-compliant code. (DOC) Same plan, Task 14: replaced the stale defer goleak.VerifyNone(t) pattern with goleak.IgnoreCurrent() baseline; updated the comment to explain why minio-go's HTTP keepalive goroutines (IdleConnTimeout=60s) require it. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 61f128a commit ee87139

46 files changed

Lines changed: 7260 additions & 321 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/superpowers/plans/2026-04-30-natsrouter-improvements.md

Lines changed: 1001 additions & 0 deletions
Large diffs are not rendered by default.

docs/superpowers/plans/2026-05-06-mongoutil-extension-and-minioutil.md

Lines changed: 2822 additions & 0 deletions
Large diffs are not rendered by default.

docs/superpowers/specs/2026-05-06-mongoutil-extension-and-miniout-design.md

Lines changed: 607 additions & 0 deletions
Large diffs are not rendered by default.

go.mod

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ require (
99
github.com/docker/docker v27.1.1+incompatible
1010
github.com/elastic/go-elasticsearch/v8 v8.19.3
1111
github.com/gin-gonic/gin v1.12.0
12+
github.com/go-resty/resty/v2 v2.17.2
1213
github.com/gocql/gocql v1.7.0
1314
github.com/google/uuid v1.6.0
15+
github.com/minio/minio-go/v7 v7.1.0
1416
github.com/nats-io/jwt/v2 v2.8.1
1517
github.com/nats-io/nats-server/v2 v2.12.6
1618
github.com/nats-io/nats.go v1.50.0
@@ -20,15 +22,18 @@ require (
2022
github.com/redis/go-redis/v9 v9.18.0
2123
github.com/stretchr/testify v1.11.1
2224
github.com/testcontainers/testcontainers-go v0.42.0
25+
github.com/testcontainers/testcontainers-go/modules/minio v0.42.0
2326
github.com/testcontainers/testcontainers-go/modules/mongodb v0.42.0
2427
github.com/testcontainers/testcontainers-go/modules/nats v0.42.0
2528
go.mongodb.org/mongo-driver/v2 v2.5.0
29+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
2630
go.opentelemetry.io/otel v1.43.0
2731
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
2832
go.opentelemetry.io/otel/exporters/prometheus v0.65.0
2933
go.opentelemetry.io/otel/sdk v1.43.0
3034
go.opentelemetry.io/otel/sdk/metric v1.43.0
3135
go.opentelemetry.io/otel/trace v1.43.0
36+
go.uber.org/goleak v1.3.0
3237
go.uber.org/mock v0.6.0
3338
golang.org/x/crypto v0.49.0
3439
golang.org/x/sync v0.20.0
@@ -57,11 +62,13 @@ require (
5762
github.com/distribution/reference v0.6.0 // indirect
5863
github.com/docker/go-connections v0.6.0 // indirect
5964
github.com/docker/go-units v0.5.0 // indirect
65+
github.com/dustin/go-humanize v1.0.1 // indirect
6066
github.com/ebitengine/purego v0.10.0 // indirect
6167
github.com/elastic/elastic-transport-go/v8 v8.8.0 // indirect
6268
github.com/felixge/httpsnoop v1.0.4 // indirect
6369
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
6470
github.com/gin-contrib/sse v1.1.1 // indirect
71+
github.com/go-ini/ini v1.67.0 // indirect
6572
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
6673
github.com/go-logr/logr v1.4.3 // indirect
6774
github.com/go-logr/stdr v1.2.2 // indirect
@@ -78,11 +85,14 @@ require (
7885
github.com/json-iterator/go v1.1.12 // indirect
7986
github.com/klauspost/compress v1.18.5 // indirect
8087
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
88+
github.com/klauspost/crc32 v1.3.0 // indirect
8189
github.com/leodido/go-urn v1.4.0 // indirect
8290
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
8391
github.com/magiconair/properties v1.8.10 // indirect
8492
github.com/mattn/go-isatty v0.0.20 // indirect
93+
github.com/minio/crc64nvme v1.1.1 // indirect
8594
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
95+
github.com/minio/md5-simd v1.1.2 // indirect
8696
github.com/moby/docker-image-spec v1.3.1 // indirect
8797
github.com/moby/go-archive v0.2.0 // indirect
8898
github.com/moby/moby/api v1.54.1 // indirect
@@ -99,15 +109,18 @@ require (
99109
github.com/opencontainers/go-digest v1.0.0 // indirect
100110
github.com/opencontainers/image-spec v1.1.1 // indirect
101111
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
112+
github.com/philhofer/fwd v1.2.0 // indirect
102113
github.com/pmezard/go-difflib v1.0.0 // indirect
103114
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
104115
github.com/prometheus/common v0.67.5 // indirect
105116
github.com/prometheus/otlptranslator v1.0.0 // indirect
106117
github.com/prometheus/procfs v0.20.1 // indirect
107118
github.com/quic-go/qpack v0.6.0 // indirect
108119
github.com/quic-go/quic-go v0.59.0 // indirect
120+
github.com/rs/xid v1.6.0 // indirect
109121
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
110122
github.com/sirupsen/logrus v1.9.4 // indirect
123+
github.com/tinylib/msgp v1.6.1 // indirect
111124
github.com/tklauser/go-sysconf v0.3.16 // indirect
112125
github.com/tklauser/numcpus v0.11.0 // indirect
113126
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
@@ -117,14 +130,15 @@ require (
117130
github.com/xdg-go/stringprep v1.0.4 // indirect
118131
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
119132
github.com/yusufpapurcu/wmi v1.2.4 // indirect
133+
github.com/zeebo/xxh3 v1.1.0 // indirect
120134
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
121-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
122135
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
123136
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
124137
go.opentelemetry.io/otel/metric v1.43.0 // indirect
125138
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
126139
go.uber.org/atomic v1.11.0 // indirect
127140
go.yaml.in/yaml/v2 v2.4.4 // indirect
141+
go.yaml.in/yaml/v3 v3.0.4 // indirect
128142
golang.org/x/arch v0.25.0 // indirect
129143
golang.org/x/net v0.52.0 // indirect
130144
golang.org/x/oauth2 v0.36.0 // indirect

go.sum

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM
6363
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
6464
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
6565
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
66+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
67+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
6668
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
6769
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
6870
github.com/elastic/elastic-transport-go/v8 v8.8.0 h1:7k1Ua+qluFr6p1jfJjGDl97ssJS/P7cHNInzfxgBQAo=
@@ -77,6 +79,8 @@ github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko
7779
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
7880
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
7981
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
82+
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
83+
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
8084
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
8185
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
8286
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -94,6 +98,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
9498
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
9599
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
96100
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
101+
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
102+
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
97103
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
98104
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
99105
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
@@ -121,8 +127,11 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
121127
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
122128
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
123129
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
130+
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
124131
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
125132
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
133+
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
134+
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
126135
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
127136
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
128137
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -140,8 +149,14 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S
140149
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
141150
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
142151
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
152+
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
153+
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
143154
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk=
144155
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
156+
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
157+
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
158+
github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8=
159+
github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA=
145160
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
146161
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
147162
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
@@ -183,6 +198,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
183198
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
184199
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
185200
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
201+
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
202+
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
186203
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
187204
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
188205
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
@@ -205,6 +222,8 @@ github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfS
205222
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
206223
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
207224
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
225+
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
226+
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
208227
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
209228
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
210229
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
@@ -224,10 +243,14 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
224243
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
225244
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
226245
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
246+
github.com/testcontainers/testcontainers-go/modules/minio v0.42.0 h1:8yTWNv8ALG7JQHYvm1n9PegH0uJT7dRtWNHf6eQeTRs=
247+
github.com/testcontainers/testcontainers-go/modules/minio v0.42.0/go.mod h1:bcjonmVMA/aEzxFFIh/FRwSkeZ+fnxwvkGN/Z4EiW28=
227248
github.com/testcontainers/testcontainers-go/modules/mongodb v0.42.0 h1:jX10Aprgf1L+Ov+KxcheZ/1JXdiJ/3wdevfWFSkxm6s=
228249
github.com/testcontainers/testcontainers-go/modules/mongodb v0.42.0/go.mod h1:Ph+xH0hAC6djPFTjPgLa3VmSfE4h82kzVIKxTj3n2o4=
229250
github.com/testcontainers/testcontainers-go/modules/nats v0.42.0 h1:WQR0+1r4GkM5QgOBoLxlP41empovt5PxtaiDpC0G7ow=
230251
github.com/testcontainers/testcontainers-go/modules/nats v0.42.0/go.mod h1:ZAI9iisjDNJmcRcycQFKSLpiBN9u2g1v9AJRq1afriE=
252+
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
253+
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
231254
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
232255
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
233256
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
@@ -247,8 +270,10 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
247270
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
248271
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
249272
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
250-
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
251-
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
273+
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
274+
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
275+
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
276+
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
252277
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
253278
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
254279
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
@@ -283,6 +308,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
283308
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
284309
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
285310
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
311+
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
312+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
286313
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
287314
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
288315
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//go:build integration
2+
3+
package mongorepo
4+
5+
import (
6+
"testing"
7+
8+
"go.mongodb.org/mongo-driver/v2/mongo"
9+
10+
"github.com/hmchangw/chat/pkg/testutil"
11+
)
12+
13+
func setupMongo(t *testing.T) *mongo.Database {
14+
return testutil.MongoDB(t, "history_service_test")
15+
}

history-service/internal/mongorepo/subscription.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ import (
88
"go.mongodb.org/mongo-driver/v2/mongo"
99

1010
"github.com/hmchangw/chat/pkg/model"
11+
"github.com/hmchangw/chat/pkg/mongoutil"
1112
)
1213

1314
const subscriptionsCollection = "subscriptions"
1415

1516
type SubscriptionRepo struct {
16-
subscriptions *Collection[model.Subscription]
17+
subscriptions *mongoutil.Collection[model.Subscription]
1718
}
1819

1920
func NewSubscriptionRepo(db *mongo.Database) *SubscriptionRepo {
2021
return &SubscriptionRepo{
21-
subscriptions: NewCollection[model.Subscription](db.Collection(subscriptionsCollection)),
22+
subscriptions: mongoutil.NewCollection[model.Subscription](db.Collection(subscriptionsCollection)),
2223
}
2324
}
2425

@@ -31,7 +32,7 @@ func (r *SubscriptionRepo) GetSubscription(ctx context.Context, account, roomID
3132
func (r *SubscriptionRepo) GetHistorySharedSince(ctx context.Context, account, roomID string) (*time.Time, bool, error) {
3233
sub, err := r.subscriptions.FindOne(ctx,
3334
bson.M{"u.account": account, "roomId": roomID},
34-
WithProjection(bson.M{"historySharedSince": 1, "_id": 0}),
35+
mongoutil.WithProjection(bson.M{"historySharedSince": 1, "_id": 0}),
3536
)
3637
if err != nil {
3738
return nil, false, err

0 commit comments

Comments
 (0)