From fd9546b4214bfb28068161077c51deafa4c81d0a Mon Sep 17 00:00:00 2001 From: Muhideen Mujeeb Adeoye Date: Wed, 2 Sep 2026 21:43:04 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20build(go):=20upgrade?= =?UTF-8?q?=20to=20Go=201.27=20and=20refresh=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add generic registry and extension methods, worker profile labels, deterministic HTTP and runtime tests, and targeted standard-library modernizations. Preserve ordered error switches and check MySQL queue iteration errors before sampling. Refresh database drivers, OpenTelemetry, CLI dependencies, and lint configuration. Include CLI and telemetry in the verification gate and document JSON compatibility and repeatable benchmark results. Validation: full live verification passed 1,058 admission/API assertions and 36 scenarios (96 assertions), with two existing announced MySQL checks skipped. Race checks, changed-code lint, vulnerability scanning, and module checksum verification passed. Core tests and MySQL vet passed after the final review fixes. --- .golangci.yml | 16 + README.md | 3 + conformance/CAPABILITY_REGISTER.md | 10 +- conformance/EVIDENCE.md | 11 + conformance/TEST_INVENTORY.tsv | 11 +- docs/benchmarks/go127-20260902/default.txt | 80 ++++ .../go127-20260902/legacy-json-vs-default.txt | 24 ++ .../benchmarks/go127-20260902/legacy-json.txt | 80 ++++ .../previous-allocator-vs-default.txt | 26 ++ .../go127-20260902/previous-allocator.txt | 80 ++++ docs/go-generics.md | 123 ++++++ docs/go-json-v2.md | 57 +++ docs/go-upgrade-performance.md | 96 +++++ docs/go-worker-diagnostics.md | 91 +++++ docs/sdk/go/overview.mdx | 2 + docs/sdk/go/runner.mdx | 2 +- docs/task-data.md | 8 +- examples/go/go.mod | 14 +- examples/go/go.sum | 43 +- go/batch_handler_test.go | 76 +++- go/driver/headgatemysql/go.mod | 15 +- go/driver/headgatemysql/go.sum | 43 +- go/driver/headgatemysql/inspect.go | 3 + go/driver/headgatepgx/go.mod | 17 +- go/driver/headgatepgx/go.sum | 43 +- go/driver/headgateredis/go.mod | 15 +- go/driver/headgateredis/go.sum | 43 +- go/driver/headgateredis/store.go | 15 +- go/go.mod | 2 +- go/go.work | 6 +- go/go.work.sum | 5 +- go/headgate.go | 9 +- go/headgateapi/api.go | 9 +- go/headgateapi/api_test.go | 92 +++++ go/headgateapi/cmd/hg-go-api/main.go | 7 +- go/headgateapi/go.mod | 32 +- go/headgateapi/go.sum | 41 +- go/headgatecrypto/go.mod | 8 +- go/headgatecrypto/go.sum | 28 +- go/headgatectl/go.mod | 6 +- go/headgatectl/go.sum | 9 +- go/headgatectl/main_test.go | 48 +++ go/headgatemigrate/go.mod | 11 +- go/headgatemigrate/go.sum | 32 +- go/headgateotel/go.mod | 16 +- go/headgateotel/go.sum | 60 ++- go/headgateshared/json_v2_test.go | 93 +++++ go/headgatetest/go.mod | 15 +- go/headgatetest/go.sum | 49 ++- go/headgateui/go.mod | 2 +- go/headgateworkflow/go.mod | 2 +- go/headgateworkflow/go.sum | 28 +- go/headgateworkflow/workflow.go | 12 +- go/insert_hook.go | 6 +- go/runtime.go | 41 ++ go/runtime_benchmark_test.go | 61 +++ go/runtime_test.go | 376 ++++++++++++++---- go/task_data.go | 24 +- go/task_data_test.go | 61 +++ scripts/bench-go-runtime.sh | 30 ++ scripts/verify.sh | 6 +- 61 files changed, 1865 insertions(+), 409 deletions(-) create mode 100644 .golangci.yml create mode 100644 docs/benchmarks/go127-20260902/default.txt create mode 100644 docs/benchmarks/go127-20260902/legacy-json-vs-default.txt create mode 100644 docs/benchmarks/go127-20260902/legacy-json.txt create mode 100644 docs/benchmarks/go127-20260902/previous-allocator-vs-default.txt create mode 100644 docs/benchmarks/go127-20260902/previous-allocator.txt create mode 100644 docs/go-generics.md create mode 100644 docs/go-json-v2.md create mode 100644 docs/go-upgrade-performance.md create mode 100644 docs/go-worker-diagnostics.md create mode 100644 go/headgateshared/json_v2_test.go create mode 100644 scripts/bench-go-runtime.sh diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..e2e94a2 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,16 @@ +version: "2" + +run: + timeout: 5m + +linters: + default: none + enable: + - govet + - ineffassign + - staticcheck + - unused + +# Apply the new gate to changes while existing lint debt is addressed separately. +issues: + new-from-rev: HEAD diff --git a/README.md b/README.md index 246d834..7142ce4 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,9 @@ worker, and enqueue setup. ### Go +Requires Go 1.27 or newer. See the [Go generics guide](docs/go-generics.md) for +typed APIs and the Go 1.27 migration. + ```go type WelcomeEmail struct { Address string `json:"address"` diff --git a/conformance/CAPABILITY_REGISTER.md b/conformance/CAPABILITY_REGISTER.md index 53594e7..726d8f7 100644 --- a/conformance/CAPABILITY_REGISTER.md +++ b/conformance/CAPABILITY_REGISTER.md @@ -152,6 +152,7 @@ not think to name. | Admission rejections by policy | ✅ | §10 **Round 32j — NO EVIDENCE, and the metric does not exist.** `admission_rejections` is declared on the history bucket in `api/headgate.openapi.yaml`, but `HistoryBucket` carries only `{at_ms, arrived, completed}` in both cores and no adapter populates it; its facade counterpart `Event::Rejected { queue, policy, count }` is declared in both languages and CONSTRUCTED NOWHERE — the same dead-variant shape round 32i found for `Event::Evicted`. (Per-job `blocked_by` attribution does exist and is tested; that is the Admission explain row, not this one.) Recorded as `none:` in `conformance/EVIDENCE.md` **Round 32k — `Event::Rejected` IS CONSTRUCTED NOW, and where it is NOT is the interesting half.** The obvious home is the admission gate, and that is exactly where it cannot go: fairness, rate class, concurrency ceilings, quarantine and queue pause are all decided INSIDE `admit.sql`/`admit.lua`, in the statement that claims the job, and none of them is returned — surfacing a per-candidate rejection means returning rejected rows out of the atomic claim and paying for it on every admit of every worker, to feed a counter. So the emission sits on the one policy rejection a RUNTIME observes: the `Outcome::RateLimited` transition (§11.2's handler-declared 429, §9.6's `IsFailure` declining), tagged `rate_class` — the §5.1 explain vocabulary's own word for that clause, so a rejections-by-policy dashboard and `GET /jobs/{id}/admission` use one word for one thing. Per-job with `count: 1`, affordable only because it rides an ack that has already made a store round trip. Both tests drive the real `process_one`/`processOne` (not the emission helper, which would still pass after someone deleted the call) and carry a control: a REAL failure takes the retry arm and emits nothing. **STILL NOT DONE, deliberately:** `admission_rejections` on the OpenAPI history bucket is unpopulated, `HistoryBucket` still carries `{at_ms, arrived, completed}` in both cores, and nothing counts the gate's own rejections | | Tracing / OTel | ✅ | §8.4 **The previously empty `go/headgateotel` directory and absent `crates/headgate-otel` crate are now real opt-in adapters.** Both consume the existing facade without changing core: a `job_span` becomes one historical OpenTelemetry `Consumer` span named `headgate.process`, with the envelope's valid W3C context installed as a remote parent, explicit event start/end timestamps, bounded span name, identity/outcome attributes and failure status. Runtime counters, duration histograms, saturation gauges and memory-guard signals reach application-owned meters; job ids and fingerprints never become metric labels. The host owns providers, SDK lifecycle, sampling, resources and exporters. In-memory SDK tests in both languages assert the parent id, remote bit, timing, status and representative metric names. Go uses OpenTelemetry v1.41, the newest release compatible with the repository's Go 1.24 minimum; Rust uses the API crate only at runtime. SDK crates remain adapter test dependencies and the no-exporter core dependency gate remains green. | | **Trace context on the envelope** | ✅ | **Round 32 — the 🔶 was "it is not specified, so implementations will diverge", and an unwritten convention is two conventions.** Now specified in §8.4 and implemented in both: `traceparent` and `tracestate` are RESERVED envelope header keys, spelled lowercase (W3C defines them as HTTP field names, canonically lowercase; the envelope's map is NOT case-insensitive so the spec must pick one — `Traceparent` is an ordinary opaque header, asserted). Producers set them at enqueue; the runtime parses `traceparent` at DISPATCH. **LENIENT MEANS LENIENT ABOUT THE CONSEQUENCE AND STRICT ABOUT THE FORMAT**: `00-{32 lowercase hex}-{16 lowercase hex}-{2 hex}`, all-zero trace-id/span-id invalid, wrong version / uppercase hex / wrong lengths / extra fields all invalid — and every invalid value is treated as ABSENT, never an enqueue error and never a dispatch failure. To the store the headers stay OPAQUE BYTES, so an invalid value round-trips out byte-identical and only the parse drops it. NO OTel SDK: ~30 lines of hex validation per core, `scripts/check-deps.sh` still green. **This required the envelope to actually carry headers**, which it did in the proto (field 20) and in the PG schema and in no adapter: added `Envelope.headers` (Rust `BTreeMap` and Go `map[string]string`, both JSON-encoded key-sorted, Go with `SetEscapeHTML(false)` because Go escapes `<>&` and serde_json does not — the Redis Go-vs-Rust keyspace byte-diff would have caught it, and this is why the codec is ONE function in the Go core rather than four copies), a `headers JSON` column on MySQL (applied live via the harness), and one ADDITIVE `j.headers` RETURNING column on `admit.sql` (a new output column only — no CTE, predicate, or pass touched; both drivers read by NAME; `test-admission.sh` run before and after). Redis needed NO admit.lua change at all (the store HGETALLs the job hash after the atomic claim); `enqueue.lua` grew a TRAILING ARGUMENT BLOCK rather than an 18th per-job field, deliberately — every existing index is `2 + i * F + k`, so widening the stride F would have silently moved all seventeen. Surfaces in exactly two places: the handler's ctx (`JobCtx::trace()` / `headgate.TraceContextFrom(ctx)`, `None` for absent AND malformed alike — a handler that behaved differently for a typo'd header is a worse bug than a missing link) and the §8.4 job-span hook. **Facade growth is ADDITIVE with a stated reason**: Rust `Event` became `#[non_exhaustive]` and gained a `JobSpan` variant (without that attribute every future signal is a breaking change for anyone with an exhaustive `match`, which makes "do not emit it" the cheap option); Go's `Event` is a struct so new fields are free. `JobSpan` fires at the END of the attempt carrying the parsed parent + an absolute start + a duration, NOT as a start/stop pair, because a facade has no span object to hand back and a start-only callback forces every bridge to keep a job-id→span map and leak one whenever a worker is killed mid-attempt; an OTel builder takes explicit timestamps, so one event needs no state. Asserted: identical vector tables in both cores (13 invalid forms), and cross-language in the conformance script over PG **both directions** (Go enqueues → Rust reads back the identical value, and the reverse) plus the invalid case reading as absent in BOTH while the raw header survives verbatim; over Redis the same round trip plus "a header-less job writes no `headers` field at all" (the trailing block is additive in the byte sense too); and a live worker loop in both languages asserting the ctx accessor and the span hook from inside a real dispatch **Round 32h — the vacuity audit.** `a header-less job writes no headers field at all` read `HEXISTS`, which is 0 for a job that does not exist — so a failed enqueue proved the contract. The job hash's existence is now the witness. Proven by skipping the enqueue: the guard fires. | +| Go worker profiles | ✅ | Go 1.27 runtime diagnostics: worker, duty, queue and task-kind pprof labels follow dispatch and tracked child goroutines; application-owned profile capture uses the standard `goroutine` and `goroutineleak` profiles. No profile listener is installed by core. `TestWorkerProfileLabelsFollowDispatchAndRestoreCaller` verifies profile output, inheritance and restoration. | | Metrics facade | ✅ | §8.4 **Round 32k:** `Rejected` joins `Evicted`, `WorkerSaturation` and `JobSpan` in the set an actual test OBSERVES — it was declared in both cores and constructed nowhere, the same dead-variant shape round 32i found for `Evicted`. `Admitted`, `Completed` and `Quarantined` are still emitted and asserted nowhere | | **Subscriptions (app-facing event stream)** | ✅ | **Round 32ag: both runtimes now expose a bounded `EventBus` distinct from §8.4 telemetry.** Owned `JobEvent`s cover successful completion, persisted failure (retryable/archive/undecodable with error), and handler revoke/cancel, and are published ONLY after the fenced Store transition succeeds; retention-zero completion and revoke report `deleted` honestly. Each subscription chooses a finite buffer and optional kind filter. Fanout is non-blocking: a full subscriber drops locally, increments its own monotone `dropped` / `Dropped` counter, and cannot delay the worker or another subscriber. Parity tests drain complete/fail/revoke jobs through real dispatch, require the unfiltered stream's three exact kinds/states, a completed-only stream's one event, and a one-slot slow stream's exact two drops while the drain completes under a deadline. Dropping/closing and reconnecting yields no old events; a new completion does arrive. **Mutation teeth:** deleting the full-buffer counter increment made both tests report zero instead of two. Delivery is deliberately process-local with no cursor, replay, or cross-process promise; callers reconcile durable state after reconnect, and wait-for-completion will use subscribe-before-enqueue plus that read. Rust drop unregisters; Go context cancel/`Close` unregisters and focused tests pass `-race`. See `docs/subscriptions.md`. | | Per-queue history | ✅ | §10 | @@ -174,7 +175,7 @@ not think to name. | Leader resign on request | ✅ | **Round 32ak:** `resign` is a consume-once heartbeat command in both runtimes. The worker keeps admitting jobs but releases each singleton duty with the store's holder-fenced release; a non-holder cannot release another node's lease. Rust/Go live worker tests wait for scheduler ownership, send resign, require immediate takeover, and prove a late former-holder release cannot delete the contender's lease. The same release/takeover contract runs in all six store/language cells; API validation, OpenAPI, and the console action include the command. | | **ORM interop (Bun / GORM / sqlc / SeaORM)** | ✅ | **Round 32 wrote the matrix §9.4b asked for: 2 languages × 2 transactional backends (PG, MySQL), 12 assertions, ZERO new ORM dependencies** — `crates/headgate-{postgres,mysql}/tests/orm_interop.rs` and `go/driver/headgate{pgx,mysql}/orm_interop_test.go`, plus `docs/orm-interop.md`. Three cases per cell, all on the NATIVE tx types the port already accepts: (a) an app-table write + `enqueue_tx` in ONE caller-owned transaction, COMMIT → both rows exist **and the gate actually admits the job** (visible is not enough — a row that commits but never passes admission is a silent stall, so the test admits for real); (b) the same transaction ROLLED BACK → neither the app row nor the job exists and nothing is admittable, **the money assertion**: a job that survives its caller's rollback has published work that never happened; (c) handler side — the effect-key claim + an app write + the fence-verified completion in one transaction (§5.6's machinery, EXTENDED with the app-table write rather than rewritten), then a crash AFTER the commit re-delivers and claims nothing, so the effect applies exactly once. App tables are `$`-scoped per run and dropped at the end (and defensively at the start). Handles exercised: Rust `tokio_postgres::Transaction` and `mysql_async::Transaction` through the public generic `enqueue_on`; Go `pgx.Tx` and `*sql.Tx` through `WrapTx`. **One source gap the matrix exposed**: `headgatemysql.WrapTx` did not exist — `MysqlTx` wrapped an unexported `*sql.Tx` with no constructor, so `EnqueueTx` could only join a transaction headgate itself opened and the Go×MySQL cell (the GORM/Bun case) was literally unwritable. Added as the symmetric counterpart of the long-standing `headgatepgx.WrapTx`, 13 lines, no dependency, no ownership transfer. The doc claims ONLY what the matrix proves and names four gaps outright: no ORM's own API is exercised (the GORM `tx.Statement.ConnPool.(*sql.Tx)` and Bun `bun.Tx.Tx` rows document where the handle LIVES, they are not tests); `database/sql`-over-pgx on Postgres is uncovered because `headgatepgx` takes `pgx.Tx` only; Redis is absent by design (§3.1 — it declines `Transactional` rather than approximating it); and **sqlx/SeaORM have no raw-connection unwrap to perform at all** — sqlx implements the wire protocols itself and never yields a `tokio_postgres::Client`, SeaORM inherits that through sqlx, so the two honest options are a small second pool on a driver headgate accepts, or enqueue-after-commit with the job-loss window transactional enqueue exists to remove. The doc also warns against hand-rolling `INSERT INTO headgate_job`, which silently skips uniqueness (§4.4), the quarantine check (§5.2), active-partition maintenance (§5.3/§13), and the arrival counters (§5.5). Rust's handler-side surface is narrower and the doc says so: `complete_tx`/`claim_effect`/`checkpoint_tx` downcast to headgate's own handle, so case (c) opens with `store.begin()` and writes through `PgTx::client()` / `MysqlTx::conn()` — enqueue-side interop is unrestricted, completion-side joining is not yet **Round 32h — the vacuity audit.** Evidence CORRECTED: the rollback half of both cells was a loop over an always-empty slice (see the Transactional enqueue row). Both now enqueue a committed sibling in the same per-run queue and assert the admitted id set is exactly that sibling. | | Plugins (hooks + middleware as one unit) | ✅ | **Round 32ac: Rust `Plugin::{global,for_kind,for_kinds}` and Go `NewPlugin` package producer middleware plus insert hooks as one validated installable unit.** The order is explicit rather than caller-option incidental: standalone components first, then global plugins in install order, then matching kind-scoped plugins in install order. Each plugin's middleware is one contiguous wrapper and its hooks one contiguous sequential group. Scope is evaluated once at the plugin boundary, so kind mutation cannot half-activate a bundle. ANY matching envelope activates the plugin around the WHOLE atomic batch; mixed-kind enqueue is never split and produces one Store result/hook lifecycle. Scope kinds reuse the shared enqueue/registration grammar and are deduplicated; empty names/scopes and invalid kinds fail construction. Both API configs carry the same bundles into direct/manual-periodic producer paths. Tests intentionally install scoped before global, assert the complete before/after and begin/end sequence, prove nonmatches are silent, and require both jobs from a mixed batch to exist. **Mutation teeth:** swapping global/scoped middleware concatenation failed the exact order proof in both languages. See `docs/plugins.md`. | -| **CLI** | ✅ | `go/headgatectl` is a Cobra incident CLI over the bounded control API: jobs, queues, memory samples and async operations. Migration credentials remain isolated in the existing migration CLIs. | +| **CLI** | ✅ | `go/headgatectl` is a Cobra incident CLI over the bounded control API: jobs, queues, memory samples and async operations. Migration credentials remain isolated in the existing migration CLIs. Go 1.27 fake-network tests pin the 30-second client deadline through both response headers and body reads. | | **Redis Sentinel / Cluster** | ✅ | Go uses the failover/cluster clients; Rust accepts Sentinel-resolved and async cluster connections. Cluster requires one explicit prefix hash tag because the atomic gate spans fleet-global and queue-local keys. | | **Connection-count budget** | ✅ | **Round 32r: one formula, documented and live-proven in all four SQL language/backend cells.** Let `T` be the maximum simultaneous `once` / `step_once` / application callbacks that retain a transaction across every worker sharing one pool. Recommended command pool `P = T + 2`: one spare lane keeps lease renewal and worker heartbeat moving; one carries admission, checkpoints, acks, duties, inspectors, API, and ordinary enqueue. These are shared pool lanes, not two connections per worker. Exact physical bound: Postgres is `P + L`, where `L=1` per notifying Store instance and `0` poll-only; MySQL is `P` because it has no notifier. Redis has no transactional handler surface and follows its caller client's command-pool plus one pub/sub connection when enabled. Four LIVE tests (Rust/Go × Postgres/MySQL) configure `T=2`, `P=4`, capacity 6. Two synchronized `once` callbacks hold pool slots for 2.5s under a 900ms lease. Once both slots are held, each test captures the current store-issued lease deadline; only after the store clock crosses it does the test accept both jobs still `running`, and then only with deadlines later than both the captured deadline and store time (renewal got through). It simultaneously requires four plain/step siblings completed (checkpoint + ack got through) and all six duties owned by the worker. Every job must then reach durable `completed`. Pool metrics are sampled throughout and never exceed four; both Postgres cells additionally tag and count `pg_stat_activity`, requiring exactly one LISTEN session and no more than five physical sessions. The older pool-of-two tests remain the separate deadlock-freedom control. `docs/connection-budget.md` documents shared-store accounting, metrics, migrations, and the application-created nested-acquisition trap: inside a transactional callback, use its supplied transaction rather than retaining one slot and calling an ordinary store method for another. | | Queue memory usage metric | ✅ | Expensive work is explicit and bounded (1,000 jobs × 200 queues maximum), cached in the store, and ordinary queue monitoring only reads the last sample. | @@ -880,7 +881,12 @@ onto the wire. Each worker owns one shared map for dependencies; every dispatche gets a fresh job map. Job values shadow worker defaults, while explicit scope reads remain available. Rust keys by `TypeId` and returns `Arc` so a lock is never held over an await. Go keys by `reflect.Type`, stores typed boxes (including typed nils), and exposes -generic functions because the language has no generic methods. +generic functions because its original Go 1.24 baseline had no generic methods. +The Go 1.27 upgrade adds `Extensions.Set[T]`, `Get[T]`, and `Remove[T]` as +compatible entry points over the same map; package functions remain supported. +`TestExtensionMethodsShareTypedState` pins interoperability, exact interface type +keys, typed nils, and zero-value containers; `TestExtensionMethodsNilReceiver` +pins nil reads/removal and the existing insert panic. The isolation proof drives two handlers concurrently through the real worker loop. Both write the same concrete type and rendezvous before reading, so a shared map cannot pass by diff --git a/conformance/EVIDENCE.md b/conformance/EVIDENCE.md index 9d23a18..aed8580 100644 --- a/conformance/EVIDENCE.md +++ b/conformance/EVIDENCE.md @@ -58,6 +58,7 @@ evidence-debt: 0 - go-mysql: driver/headgatemysql/store_test.go::TestStickyRoutingIsStrictBoundedAndSurvivesRequeue ### Task aggregation / batch handlers +- go: batch_handler_test.go::TestRegistryBatchMethodRejectsInvalidConfiguration - rust: crates/headgate-core/src/lib.rs::admission_units_group_same_kind_and_respect_bound - rust: crates/headgate/tests/batch_handler.rs::typed_batch_handler_runs_once_and_acks_each_member_independently - rust: crates/headgate/tests/batch_handler.rs::typed_batch_handler_flushes_at_max_delay @@ -105,6 +106,7 @@ NOTE: these are the security/shape contract. A live PostgreSQL maintenance run i ### **CLI** - go: headgatectl/main_test.go::TestClientUsesBoundedControlAPIAndBearerAuthentication +- go: headgatectl/main_test.go::TestClientTimeoutCancelsRequestAndBodyRead NOTE: command construction is compile-tested with Cobra; the client test pins the API-only boundary and bearer propagation. ### **Redis Sentinel / Cluster** @@ -163,6 +165,7 @@ NOTE: all six store/language cells assert newest-first retention at exactly 100 ## Enqueue ### Typed enqueue +- go: runtime_test.go::TestRegistryMethodsPreserveValidationAndDispatch - rust: crates/headgate/tests/derive.rs::derive_generates_identity_and_json_codec - rust: crates/headgate/tests/runtime.rs::drain_success_retry_panic_and_control_outcomes - go: driver/headgatepgx/runtime_test.go::TestGoRuntimeDrainStepsAndPanics @@ -642,6 +645,8 @@ NOTE: round 32l. Deleting the lost-lease check from `extend_lease` (`let _ = los - rust: crates/headgate/tests/task_data.rs::extensions_are_keyed_and_retrieved_by_concrete_type - rust: crates/headgate/tests/task_data.rs::concurrent_jobs_have_isolated_typed_data_and_it_never_enters_the_envelope - go: task_data_test.go::TestExtensionsAreKeyedAndRetrievedByConcreteType +- go: task_data_test.go::TestExtensionMethodsShareTypedState +- go: task_data_test.go::TestExtensionMethodsNilReceiver - go: headgatetest/task_data_test.go::TestConcurrentJobsHaveIsolatedTypedDataAndItNeverEntersTheEnvelope NOTE: round 32y. The container tests pin concrete-type lookup, wrong-type misses, replacement and removal; Go additionally requires `SetJobData` outside a handler to return `ErrTaskDataUnavailable` rather than falling back to a global. The runtime tests drive the real concurrent worker loop, not a type-map unit helper: two jobs store the SAME concrete type, both rendezvous before either reads, and each must recover its own marker while still seeing the unchanged worker default. They also require successful ack and inspect the retained envelope for the marker, so the proof covers dispatch lifetime and the persistence boundary together. Making the job map reuse the worker map failed both tests; the focused Go tests also pass `-race`. This is the storage substrate only—handler-parameter extraction remains a separate ❌ row. @@ -1084,6 +1089,7 @@ NOTE: the two live Postgres control-channel tests deliver `restart` through a ru - go: driver/headgatepgx/store_test.go::TestNotifyWakesAWaitingSubscriber - go: driver/headgateredis/store_test.go::TestEnqueuePublishWakesAWaitingSubscriber - rust: crates/headgate-api/tests/api.rs::sse_events_stream_queue_activity +- go: headgateapi/api_test.go::TestEventStreamHeartbeatCoalescingAndCancellation NOTE: all four prove `wait_wakeup`/`WaitWakeup` returns on an enqueue (plus the NOTIFYING cap); nothing asserts the row's claim that a worker loop's poll wait is actually shortcut by it. ## Observability @@ -1149,6 +1155,11 @@ NOTE: the runtime tests prove the `JobSpan` hook fires exactly once with the par - sh: xlang §8.4: Go enqueues traceparent -> Rust reads back the IDENTICAL value - sh: xlang §8.4 (Redis): a header-less job writes no headers field at all +### Go worker profiles +- go: runtime_test.go::TestWorkerProfileLabelsFollowDispatchAndRestoreCaller +- go: runtime_test.go::TestWorkerAndDutyProfileLabels +NOTE: Go-only operational profiles. The runtime supplies labels; the embedding application owns collection. The leak profile is the standard library detector and cannot diagnose every stuck handler. + ### Metrics facade - rust: crates/headgate/src/worker.rs::a_sweep_that_deleted_rows_says_so - rust: crates/headgate/src/worker.rs::an_empty_sweep_stays_quiet diff --git a/conformance/TEST_INVENTORY.tsv b/conformance/TEST_INVENTORY.tsv index 439f5d6..696e10c 100644 --- a/conformance/TEST_INVENTORY.tsv +++ b/conformance/TEST_INVENTORY.tsv @@ -68,7 +68,7 @@ rust crates/headgate/tests/subscriptions.rs 2 rust crates/headgate/tests/task_data.rs 2 rust crates/headgate/tests/tracked_tasks.rs 5 go go/admission_unit_test.go 1 -go go/batch_handler_test.go 3 +go go/batch_handler_test.go 4 go go/circuit_breaker_test.go 4 go go/client_test.go 4 go go/cronspec_test.go 4 @@ -92,15 +92,16 @@ go go/driver/headgateredis/inspect_test.go 2 go go/driver/headgateredis/store_test.go 7 go go/enqueue_middleware_test.go 4 go go/fingerprint_test.go 2 -go go/headgateapi/api_test.go 18 +go go/headgateapi/api_test.go 19 go go/headgatecrypto/crypto_test.go 3 -go go/headgatectl/main_test.go 1 +go go/headgatectl/main_test.go 2 go go/headgatemigrate/cmd/hg-migrate/main_test.go 5 go go/headgatemigrate/live_mysql_test.go 2 go go/headgatemigrate/live_postgres_test.go 1 go go/headgatemigrate/migrate_test.go 4 go go/headgateotel/otel_test.go 2 go go/headgateshared/codec_test.go 4 +go go/headgateshared/json_v2_test.go 2 go go/headgatetest/batch_handler_test.go 1 go go/headgatetest/client_from_context_test.go 3 go go/headgatetest/database_mysql_test.go 1 @@ -118,10 +119,10 @@ go go/plugin_test.go 3 go go/postgressql/namespace_test.go 4 go go/progress_test.go 2 go go/result_test.go 2 -go go/runtime_test.go 14 +go go/runtime_test.go 17 go go/schedulespec_test.go 1 go go/subscription_test.go 2 -go go/task_data_test.go 1 +go go/task_data_test.go 3 go go/tracecontext_test.go 4 go go/validate_test.go 10 sh scripts/test-admission.sh 545 diff --git a/docs/benchmarks/go127-20260902/default.txt b/docs/benchmarks/go127-20260902/default.txt new file mode 100644 index 0000000..8c67672 --- /dev/null +++ b/docs/benchmarks/go127-20260902/default.txt @@ -0,0 +1,80 @@ +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 129189 1825 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 127676 1898 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 201837 1180 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 131478 1758 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 131521 1864 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 201735 1188 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 121776 1806 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 131298 1812 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 196459 1179 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 125052 1780 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 128444 1859 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 199689 1215 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 132097 1775 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 126903 1816 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 206232 1181 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 133269 1751 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 129523 1833 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 203760 1173 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 133440 1782 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 132100 1830 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 205899 1187 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 135367 1787 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 131401 1821 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 205000 1188 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 132212 1763 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 129570 1844 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 205252 1194 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 132861 1759 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 132075 1839 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 196597 1197 ns/op 1248 B/op 3 allocs/op +PASS diff --git a/docs/benchmarks/go127-20260902/legacy-json-vs-default.txt b/docs/benchmarks/go127-20260902/legacy-json-vs-default.txt new file mode 100644 index 0000000..5cd5f82 --- /dev/null +++ b/docs/benchmarks/go127-20260902/legacy-json-vs-default.txt @@ -0,0 +1,24 @@ +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro + │ legacy-json.txt │ default.txt │ + │ sec/op │ sec/op vs base │ +DecodeArgs1K 5.981µ ± 1% 1.778µ ± 2% -70.28% (p=0.000 n=10) +TypedDispatch1K 6.042µ ± 1% 1.836µ ± 2% -69.61% (p=0.000 n=10) +MarshalArgs1K 1.448µ ± 2% 1.188µ ± 1% -17.99% (p=0.000 n=10) +geomean 3.740µ 1.571µ -58.00% + + │ legacy-json.txt │ default.txt │ + │ B/op │ B/op vs base │ +DecodeArgs1K 1.320Ki ± 0% 1.094Ki ± 0% -17.16% (p=0.000 n=10) +TypedDispatch1K 1.477Ki ± 0% 1.250Ki ± 0% -15.34% (p=0.000 n=10) +MarshalArgs1K 1.172Ki ± 0% 1.219Ki ± 0% +4.00% (p=0.000 n=10) +geomean 1.317Ki 1.186Ki -9.99% + + │ legacy-json.txt │ default.txt │ + │ allocs/op │ allocs/op vs base │ +DecodeArgs1K 8.000 ± 0% 3.000 ± 0% -62.50% (p=0.000 n=10) +TypedDispatch1K 9.000 ± 0% 4.000 ± 0% -55.56% (p=0.000 n=10) +MarshalArgs1K 2.000 ± 0% 3.000 ± 0% +50.00% (p=0.000 n=10) +geomean 5.241 3.302 -37.00% diff --git a/docs/benchmarks/go127-20260902/legacy-json.txt b/docs/benchmarks/go127-20260902/legacy-json.txt new file mode 100644 index 0000000..12f4dcc --- /dev/null +++ b/docs/benchmarks/go127-20260902/legacy-json.txt @@ -0,0 +1,80 @@ +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 39598 5979 ns/op 1352 B/op 8 allocs/op +BenchmarkTypedDispatch1K 39650 6107 ns/op 1512 B/op 9 allocs/op +BenchmarkMarshalArgs1K 168183 1449 ns/op 1200 B/op 2 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 39522 5983 ns/op 1352 B/op 8 allocs/op +BenchmarkTypedDispatch1K 39613 6043 ns/op 1512 B/op 9 allocs/op +BenchmarkMarshalArgs1K 167858 1443 ns/op 1200 B/op 2 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 40575 6037 ns/op 1352 B/op 8 allocs/op +BenchmarkTypedDispatch1K 38998 6183 ns/op 1512 B/op 9 allocs/op +BenchmarkMarshalArgs1K 162414 1493 ns/op 1200 B/op 2 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 39621 6013 ns/op 1352 B/op 8 allocs/op +BenchmarkTypedDispatch1K 40065 6002 ns/op 1512 B/op 9 allocs/op +BenchmarkMarshalArgs1K 167923 1443 ns/op 1200 B/op 2 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 39369 6042 ns/op 1352 B/op 8 allocs/op +BenchmarkTypedDispatch1K 39805 6040 ns/op 1512 B/op 9 allocs/op +BenchmarkMarshalArgs1K 168828 1445 ns/op 1200 B/op 2 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 40070 5969 ns/op 1352 B/op 8 allocs/op +BenchmarkTypedDispatch1K 39417 6101 ns/op 1512 B/op 9 allocs/op +BenchmarkMarshalArgs1K 165369 1475 ns/op 1200 B/op 2 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 39558 5964 ns/op 1352 B/op 8 allocs/op +BenchmarkTypedDispatch1K 40039 5995 ns/op 1512 B/op 9 allocs/op +BenchmarkMarshalArgs1K 168151 1445 ns/op 1200 B/op 2 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 40399 5921 ns/op 1352 B/op 8 allocs/op +BenchmarkTypedDispatch1K 39837 6015 ns/op 1512 B/op 9 allocs/op +BenchmarkMarshalArgs1K 164977 1447 ns/op 1200 B/op 2 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 40203 5955 ns/op 1352 B/op 8 allocs/op +BenchmarkTypedDispatch1K 40221 6004 ns/op 1512 B/op 9 allocs/op +BenchmarkMarshalArgs1K 168115 1450 ns/op 1200 B/op 2 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 40054 6031 ns/op 1352 B/op 8 allocs/op +BenchmarkTypedDispatch1K 39528 6076 ns/op 1512 B/op 9 allocs/op +BenchmarkMarshalArgs1K 166473 1460 ns/op 1200 B/op 2 allocs/op +PASS diff --git a/docs/benchmarks/go127-20260902/previous-allocator-vs-default.txt b/docs/benchmarks/go127-20260902/previous-allocator-vs-default.txt new file mode 100644 index 0000000..2b9ed0c --- /dev/null +++ b/docs/benchmarks/go127-20260902/previous-allocator-vs-default.txt @@ -0,0 +1,26 @@ +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro + │ previous-allocator.txt │ default.txt │ + │ sec/op │ sec/op vs base │ +DecodeArgs1K 1.778µ ± 3% 1.778µ ± 2% ~ (p=0.493 n=10) +TypedDispatch1K 1.860µ ± 1% 1.836µ ± 2% ~ (p=0.055 n=10) +MarshalArgs1K 1.192µ ± 2% 1.188µ ± 1% ~ (p=0.361 n=10) +geomean 1.580µ 1.571µ -0.57% + + │ previous-allocator.txt │ default.txt │ + │ B/op │ B/op vs base │ +DecodeArgs1K 1.094Ki ± 0% 1.094Ki ± 0% ~ (p=1.000 n=10) ¹ +TypedDispatch1K 1.250Ki ± 0% 1.250Ki ± 0% ~ (p=1.000 n=10) ¹ +MarshalArgs1K 1.219Ki ± 0% 1.219Ki ± 0% ~ (p=1.000 n=10) ¹ +geomean 1.186Ki 1.186Ki +0.00% +¹ all samples are equal + + │ previous-allocator.txt │ default.txt │ + │ allocs/op │ allocs/op vs base │ +DecodeArgs1K 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ +TypedDispatch1K 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=10) ¹ +MarshalArgs1K 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ +geomean 3.302 3.302 +0.00% +¹ all samples are equal diff --git a/docs/benchmarks/go127-20260902/previous-allocator.txt b/docs/benchmarks/go127-20260902/previous-allocator.txt new file mode 100644 index 0000000..21ffc82 --- /dev/null +++ b/docs/benchmarks/go127-20260902/previous-allocator.txt @@ -0,0 +1,80 @@ +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 130798 1792 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 129874 1854 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 202418 1184 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 133468 1773 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 130731 1872 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 188965 1210 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 126289 1829 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 130618 1846 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 201003 1242 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 132963 1774 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 128983 1846 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 202609 1194 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 130538 1771 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 127615 1871 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 204013 1190 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 132496 1808 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 129124 1881 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 197077 1196 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 132036 1773 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 131220 1830 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 204064 1183 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 134818 1754 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 130492 1874 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 203404 1179 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 125384 1782 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 130084 1857 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 203650 1183 ns/op 1248 B/op 3 allocs/op +PASS +goos: darwin +goarch: arm64 +pkg: github.com/mujhtech/headgate/go +cpu: Apple M2 Pro +BenchmarkDecodeArgs1K 131442 1823 ns/op 1120 B/op 3 allocs/op +BenchmarkTypedDispatch1K 125700 1863 ns/op 1280 B/op 4 allocs/op +BenchmarkMarshalArgs1K 203620 1194 ns/op 1248 B/op 3 allocs/op +PASS diff --git a/docs/go-generics.md b/docs/go-generics.md new file mode 100644 index 0000000..19ce580 --- /dev/null +++ b/docs/go-generics.md @@ -0,0 +1,123 @@ +# Go 1.27 and generics in headgate + +All Go modules, including the examples, require Go 1.27.0 or newer. CI and release +builds select their toolchain from `go/go.work`. This upgrades the language baseline; +it does not request newer third-party dependency versions. + +[Go 1.27](https://go.dev/doc/go1.27) adds methods with their own type parameters. +Interface methods still cannot declare type parameters, and generic methods cannot +implement interface methods. This makes concrete SDK objects a useful place to adopt +the feature while preserving headgate's existing store and handler contracts. + +## Available now + +Headgate already uses generics for `Job[T]`, `Worker[T]`, `RegisterFunc[T]`, +`RegisterBatchFunc[T]`, `DecodeArgs[T]`, typed handler extractors, cursor steps, +and task-local data. Those APIs remain supported. + +`Extensions` now also offers generic methods: + +```go +type DatabasePool struct{ Name string } + +extensions := headgate.NewExtensions() +extensions.Set(DatabasePool{Name: "primary"}) // T inferred from the argument +pool, ok := extensions.Get[DatabasePool]() +removed, found := extensions.Remove[DatabasePool]() +``` + +These delegate to `SetExtension`, `Extension`, and `RemoveExtension`, sharing the +same mutex, type keys, replacement behavior, and nil handling. Existing callers +can mix both forms. The zero-value container works; reads and removal on a nil +container miss, and insertion on a nil container panics. + +Type identity is the declared `T`, including an explicitly requested interface +type. `extensions.Set[any](value)` stores under `any`, not the dynamic concrete +type of `value`. Typed nil pointers remain present values. Generics provide typed +access but do not remove the need for runtime type keys in a heterogeneous map. +See [task-local data](task-data.md) for lifetime and concurrency rules. + +`Registry` now offers `RegisterFunc[T]`, `RegisterWorker[T]`, and +`RegisterBatchFunc[T]` methods. They delegate to the existing package functions; +kind and alias validation, decoding, duplicate rejection, and batch outcomes have +one implementation. Existing package-function registrations remain supported. + +```go +registry := headgate.NewRegistry() +err := registry.RegisterFunc(func(ctx context.Context, job *headgate.Job[Invoice]) error { + return sendInvoice(ctx, job.Args) +}) // Invoice is inferred from the callback. +``` + +## Further candidates + +These are recommendations, not additional APIs shipped by this change. + +| Surface | Possible API | Benefit and constraint | +| --- | --- | --- | +| Handler extractors | Generic methods on `Registry` for the existing arities | Method syntax does not remove the fixed-arity design; generic methods are not variadic type parameters. | +| Typed producer convenience | A new `client.EnqueueTask(ctx, task, options)` | First define ID generation, encoding, fingerprinting, and options for constructing an `Envelope`; call the existing client so middleware, authorization, and backpressure still run. An `Args` parameter is sufficient unless a type parameter also connects typed inputs and outputs. | + +Prior art supports keeping typed user-facing APIs: River's `Worker[T JobArgs]` +and apalis's heterogeneous `Extensions` are recorded in the +[River](river-feature-enumeration.md) and [apalis](apalis-feature-enumeration.md) +inventories. The receiver syntax is an ergonomic improvement, not a new queue +capability. + +Keep `Store`, `TransactionalStore`, durable `Envelope`, and admission policy as +they are. A registry holds multiple task types and erases them only after typed +registration. Making the whole registry or store generic over one payload would +make mixed-kind dispatch harder without improving atomic admission. Go 1.27's +generic methods also cannot make an unsupported store operation become a +compile-time capability. + +The runtime now adds pprof labels for workers, duties and job dispatch. See +[Go worker diagnostics](go-worker-diagnostics.md) for capture and leak-profile +limitations. The memory guard, rolling drain and batch-delay tests use virtual +clock bubbles; the batch test proves both no early flush and an on-time flush. + +See [JSON v2 compatibility](go-json-v2.md) for executed migration checks and the +reason production codecs retain `encoding/json`. + +## Upgrade verification + +Verified with Go 1.27.1 against disposable PostgreSQL 17, Redis 7.4, and MySQL +8.4 instances. `scripts/verify.sh` completed `ALL GREEN`: Rust and Go tests had +zero database-gated skips; the admission and HTTP parity corpus passed 1,058 +assertions with two announced skips; 36 executable scenarios passed 96 +assertions. The evidence checker resolved 741 citations with zero evidence debt. + +The additional CLI and OpenTelemetry modules passed vet and tests. Focused +extension, runtime profiling, registration, virtual-clock and JSON compatibility +tests passed under the race detector. Examples, dependency isolation, migration +parity, and test inventory checks passed through the full gate. + +The tooling refresh installed gopls 0.23.0 and golangci-lint 2.13.2 (built with +Go 1.27). Generic-method diagnostics and lint checks pass. The root +`.golangci.yml` checks changed code with govet, staticcheck, ineffassign and unused. +See [dependency updates and measurements](go-upgrade-performance.md) for versions, +benchmark methodology, raw samples, and the limits of the performance claims. + +`go mod tidy` completed for the 12 non-core modules. Core's standalone tidy +attempt cannot resolve its existing test import of the sibling `headgatetest` +module without workspace wiring. Its dependency contract is intentionally left +unchanged; core tests above run through `go.work`, and the core dependency +isolation check passes. + +## Additional modernization + +Targeted `errors.AsType` conversions cover transport classification, insert +outcomes, API decoding/authorization, and Redis lease +errors. Existing ordered switch classifications retain `errors.As` where that +form remains clearer. The workflow's bounded read/write pools use `WaitGroup.Go`; +the worker's specialized tracking and panic-recovery machinery stays intact. + +The CLI timeout tests exercise both waiting for response headers and reading a +streamed body at the exact 30-second boundary. The API event-stream test checks +the 15-second heartbeat, 200 ms coalescing, duplicate wakeups, and client +cancellation while a store wait and coalescing timer are active. Both use +Go 1.27's in-memory `httptest.NewTestServer` with `synctest`. + +The standalone `hg-go-api` server limits requests to 128 header values, in +addition to net/http's byte limit. Applications embedding the handler continue +to own their HTTP server configuration. diff --git a/docs/go-json-v2.md b/docs/go-json-v2.md new file mode 100644 index 0000000..4b24482 --- /dev/null +++ b/docs/go-json-v2.md @@ -0,0 +1,57 @@ +# JSON v2 and headgate's wire contract + +Go 1.27 implements the existing `encoding/json` API through the new v2 engine +with compatibility options. Headgate therefore uses that engine already. +Importing `encoding/json/v2` directly selects different defaults; it is not a +necessary step to receive the engine update. + +The tests in `go/headgateshared/json_v2_test.go` execute both APIs and verify: + +| Case | Compatibility API | Default v2 API | +| --- | --- | --- | +| Nil string slice | `null` | `[]` | +| Zero integer tagged `omitempty` | Omitted | Included as `0` | +| Duplicate object keys | Later value wins | Error | +| Differently cased struct field | Matches | Does not match by default | +| Invalid UTF-8 string | Replacement character | Error | + +Compatibility tests also pin sorted map keys, HTML escaping, and empty-list +bytes. `jsonv2.Marshal(value, jsonv1.DefaultOptionsV1())` matches the current v1 +API for these fixtures. Default v2 map ordering is not deterministic; any +canonical serializer must explicitly select deterministic ordering. + +## Migration decision + +Keep existing payload, checkpoint, cursor and HTTP codecs on their compatibility +contracts. Payload bytes feed `Fingerprint`, and checkpoint/header bytes and +HTTP responses participate in cross-language conformance. Changing nil handling, +field omission, escaping or map ordering can change those bytes even when an +application sees equivalent values. Strict input validation also changes which +requests and persisted payloads are accepted. + +Adopt a direct v2 API only at a bounded boundary with explicit options and golden +tests. A general payload switch would require a versioned codec contract and a +cross-language migration plan, including user-defined marshalers. The fixture +tests cover the named cases, not all possible application payload types. + +## An upgrade-level byte difference + +An executed comparison with Go 1.27.1 found that even the compatibility API +encodes an invalid UTF-8 byte in a Go string differently under the two engines: +the default engine writes the literal replacement character, while +`GOEXPERIMENT=nojsonv2` writes `\ufffd`. Both decode to the same Unicode value, +but their byte fingerprints differ. Callers that previously serialized invalid +UTF-8 into payloads must account for this during an upgrade. The compatibility +option set preserves semantic behavior, not every historical byte spelling. + +Sources: [Go 1.27 JSON changes](https://go.dev/doc/go1.27#encodingjsonv2) and +the installed Go 1.27.1 `encoding/json/v2_options.go` migration documentation. + +## Verification + +The full repository gate passed with the default Go 1.27.1 engine against +PostgreSQL 17, Redis 7.4, and MySQL 8.4: 1,058 admission/API assertions passed +(two announced skips), and 96 scenario assertions passed. This includes the +existing Go/Rust HTTP header, raw-body and normalized-body comparisons. The +focused JSON fixture tests also passed under `-race`. These results verify the +current corpus; they do not erase the invalid-UTF-8 byte difference above. diff --git a/docs/go-upgrade-performance.md b/docs/go-upgrade-performance.md new file mode 100644 index 0000000..24b5ddb --- /dev/null +++ b/docs/go-upgrade-performance.md @@ -0,0 +1,96 @@ +# Go 1.27 measurements and dependency refresh + +Measured on September 2, 2026 with Go 1.27.1, macOS arm64, Apple M2 Pro, +GOMAXPROCS=1. Each case uses a typed job with a 1 KiB ASCII message. Ten samples +per configuration, 200 ms per sample, interleaved serially after compiling all +binaries. No verification or lint process ran during the timed measurements. + +## JSON engine + +The comparison uses the same source, dependencies, and Go 1.27.1 compiler: +`GOEXPERIMENT=nojsonv2` versus the default. Production calls remain +`encoding/json`. This isolates the engine switch; it is not a comparison of +complete Go 1.24 and 1.27 applications. + +| Operation | Legacy engine | Default engine | Time change | Allocations, old → new | +| --- | ---: | ---: | ---: | ---: | +| Typed payload decode | 5.981 µs | 1.778 µs | −70.28% | 8 → 3 | +| Typed handler dispatch | 6.042 µs | 1.836 µs | −69.61% | 9 → 4 | +| Payload encode | 1.448 µs | 1.188 µs | −17.99% | 2 → 3 | + +All three time differences are significant in benchstat (reported p=0.000, +n=10). Encoding allocates 1,248 rather than 1,200 bytes per operation: faster +does not mean lower allocation cost in every direction. Payloads with maps, +custom marshalers, binary data, or different sizes can behave differently. +Dispatch here measures decode and the registered handler adapter; it does not +include store I/O, leases, polling, or the complete worker loop. No fleet +throughput or tail-latency improvement is claimed. + +## Allocator + +Compared `GOEXPERIMENT=nosizespecializedmalloc` with the default while retaining +the default JSON engine. None of the three cases showed a statistically +significant time difference (p=0.493, 0.055, and 0.361). Allocations and bytes per +operation were unchanged. The new allocator stays enabled; these workloads do +not establish a benefit from it independently of JSON. + +Green Tea GC and container-aware GOMAXPROCS remain enabled by runtime defaults. +This benchmark does not isolate either one's effect. + +## Reproduce + +From the repository root, with Go 1.27.1 and `benchstat` on PATH: + +```sh +bash scripts/bench-go-runtime.sh /tmp/headgate-runtime-results +``` + +The script builds all configurations before timing and runs them serially. Use +an otherwise idle machine. Raw samples and benchstat reports for this run are +in [benchmarks/go127-20260902](benchmarks/go127-20260902/). + +## Dependencies + +All existing module paths keep their current major versions. Local sibling +`replace` directives remain because they wire this multi-module checkout; none +were external compatibility forks or Go 1.24 workarounds. +The workspace selects toolchain Go 1.27.1 so CI and release builds use the patch +version validated here; module language minimums remain Go 1.27.0. + +| Dependency | Previous | Updated | +| --- | --- | --- | +| pgx | 5.7.2 | 5.10.0 | +| go-redis | 9.7.0 | 9.22.0 | +| MySQL driver | 1.9.3 | 1.10.1 | +| OpenTelemetry APIs and SDKs | 1.41.0 | 1.46.0 | +| Cobra / pflag | 1.10.1 / 1.0.9 | 1.10.2 / 1.0.10 | +| x/sync | 0.10.0 | 0.22.0 | +| x/sys | 0.41.0 | 0.47.0 | +| x/text | 0.21.0 | 0.41.0 | +| testify (dependency tests) | 1.11.1 | 1.12.1 | + +The old x/crypto requirement disappears after the pgx refresh and module tidy. +pgx 5.9 raised its Go minimum to 1.25; pgx 5.8 had already removed x/crypto. +Not every update required raising our Go minimum. Protobuf was already current +at 1.36.12. Transitive requirements and checksums were resolved for each module; +core still has no database driver or exporter dependency. + +Upstream release notes: [pgx](https://github.com/jackc/pgx/blob/v5.10.0/CHANGELOG.md), +[Redis](https://github.com/redis/go-redis/releases/tag/v9.22.0), +[MySQL](https://github.com/go-sql-driver/mysql/releases/tag/v1.10.1), +[OpenTelemetry](https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.46.0). + +## Verification after dependency updates + +The complete `scripts/verify.sh` run passed with disposable PostgreSQL 17, +Redis 7.4, and MySQL 8.4: 1,058 admission/API assertions, 36 shared scenarios +(96 assertions), and 741 resolved evidence citations with zero evidence debt. +Rust and Go database tests had zero skips. Two existing MySQL pending-command +read-path checks remain announced skips in the admission corpus. CLI and +OpenTelemetry are now included in the main vet/build/test gate. + +Race checks passed for core, API, CLI, workflows, OpenTelemetry, and shared JSON +contracts. golangci-lint 2.13.2 reported zero issues for changed packages; +gopls 0.23.0 accepted the generic methods. `govulncheck` reported no known +vulnerabilities across all workspace modules. All 13 modules, including +examples, passed `go mod verify`. diff --git a/docs/go-worker-diagnostics.md b/docs/go-worker-diagnostics.md new file mode 100644 index 0000000..2c5c7ce --- /dev/null +++ b/docs/go-worker-diagnostics.md @@ -0,0 +1,91 @@ +# Go worker diagnostics + +Go runners attach standard `runtime/pprof` labels to work: + +| Label | Scope | +| --- | --- | +| `headgate.worker` | Runner identity, inherited by its goroutines | +| `headgate.role` | `worker`, `duty`, or `job` | +| `headgate.duty` | Individual maintenance duty | +| `headgate.queue` | Queue of the dispatched job | +| `headgate.kind` | Durable task kind | + +Job dispatch adds these labels even through `Drain` and `PerformOne`. Tracked child +goroutines inherit the job's labels. Caller labels remain available, and dispatch +restores them when it returns. Payloads, headers, partition keys and job IDs are +not added as labels. Applications should choose non-sensitive operational names. + +Go 1.27 includes labels in tracebacks by default. Standard goroutine and CPU +profiles can also use them to identify blocked work. A profile is process-wide; +it may include unrelated application goroutines and labels. + +## Capture on demand + +The application owns the output destination and when collection runs. Core does +not install an HTTP handler or start a diagnostic listener. + +```go +import ( + "io" + "runtime/pprof" +) + +func writeBlockedWork(w io.Writer) error { + return pprof.Lookup("goroutine").WriteTo(w, 1) +} + +func writeLeaks(w io.Writer) error { + return pprof.Lookup("goroutineleak").WriteTo(w, 1) +} +``` + +Use the goroutine profile first when a worker will not drain. The Go 1.27 +`goroutineleak` profile specifically detects goroutines blocked on unreachable +synchronization primitives. A handler waiting on a globally reachable channel, +external I/O, or a live application lock can remain stuck without appearing in +that profile. An empty leak profile does not prove the worker is healthy. + +For an application that already exposes `net/http/pprof` on its own diagnostic +server, `/debug/pprof/goroutineleak` is available there. Profile collection is an +on-demand diagnostic, not a queue-depth query or a periodic admission operation. + +See [Go 1.27 runtime changes](https://go.dev/doc/go1.27#runtime) and the existing +[stuck-handler contract](stuck-job-handlers.md). Profiling does not change lease +fencing, cancellation, or the cooperative nature of Go handler shutdown. + +## Capture the lead-up to an incident + +Applications can opt into Go's `runtime/trace.FlightRecorder` (introduced in 1.25) +to keep a rolling execution trace. Start it at application startup, then snapshot +it when a drain stalls or latency crosses an application-defined threshold: + +```go +recorder := trace.NewFlightRecorder(trace.FlightRecorderConfig{ + MinAge: 10 * time.Second, + MaxBytes: 8 << 20, +}) +if err := recorder.Start(); err != nil { + return err +} +defer recorder.Stop() + +// After the application's incident trigger, with an application-owned io.Writer: +_, err := recorder.WriteTo(destination) +``` + +Import `runtime/trace` and `time`. Keep the recorder alive for the application's +lifetime and serialize snapshots; `WriteTo` must not overlap another `WriteTo`. +`MaxBytes` takes precedence over `MinAge`, but is a hint rather than a strict cap +on memory use or snapshot size. Open a saved trace with `go tool trace incident.trace`. +This is an application integration recipe: headgate does not automatically start +a recorder, select incident thresholds, or persist trace files. Trace collection +is process-wide and has runtime overhead. + +## Container CPU quotas + +The Go runtime now accounts for Linux container CPU quotas when choosing +`GOMAXPROCS` and periodically updates it. Headgate does not override it. An +application-supplied `GOMAXPROCS` environment variable or runtime call disables +automatic updates; review deployment overrides before expecting this benefit. +This controls CPU execution parallelism, independently of headgate's configured +job concurrency and store-enforced fleet ceilings. diff --git a/docs/sdk/go/overview.mdx b/docs/sdk/go/overview.mdx index dcf14f6..4bdf409 100644 --- a/docs/sdk/go/overview.mdx +++ b/docs/sdk/go/overview.mdx @@ -8,6 +8,8 @@ The root Go module contains driver-free contracts, producer behavior, registry, steps, scheduler, middleware, plugins, and subscriptions. Drivers and optional feature layers are separate modules so applications pull only the dependencies they select. +Requires Go 1.27 or newer. + ```bash go get github.com/mujhtech/headgate/go go get github.com/mujhtech/headgate/go/driver/headgatepgx diff --git a/docs/sdk/go/runner.mdx b/docs/sdk/go/runner.mdx index b5b6f9b..8e1aceb 100644 --- a/docs/sdk/go/runner.mdx +++ b/docs/sdk/go/runner.mdx @@ -6,7 +6,7 @@ icon: "server" ```go registry := headgate.NewRegistry() -err := headgate.RegisterFunc[GenerateInvoice](registry, +err := registry.RegisterFunc[GenerateInvoice]( func(ctx context.Context, job *headgate.Job[GenerateInvoice]) error { headgate.Log(ctx, "generating "+job.Args.InvoiceID) return nil diff --git a/docs/task-data.md b/docs/task-data.md index 8d28926..125f630 100644 --- a/docs/task-data.md +++ b/docs/task-data.md @@ -46,12 +46,14 @@ same map; cloning `JobCtx` shares only that job's map. ## Go -Go does not permit generic methods, so `Extensions` is the container and its typed -operations are package functions: +With Go 1.27, `Extensions` exposes generic `Set`, `Get`, and `Remove` methods. +The original `SetExtension`, `Extension`, and `RemoveExtension` package functions +remain supported and operate on the same map: ```go extensions := headgate.NewExtensions() -headgate.SetExtension(extensions, databasePool{/* ... */}) +extensions.Set(databasePool{/* ... */}) +pool, ok := extensions.Get[databasePool]() runner := headgate.NewRunner(store, registry, headgate.Config{ Extensions: extensions, diff --git a/examples/go/go.mod b/examples/go/go.mod index bb44f01..b1e08fd 100644 --- a/examples/go/go.mod +++ b/examples/go/go.mod @@ -1,6 +1,6 @@ module github.com/mujhtech/headgate/examples/go -go 1.24.0 +go 1.27.0 require ( github.com/mujhtech/headgate/go v0.1.6 @@ -13,15 +13,15 @@ require ( require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/go-sql-driver/mysql v1.10.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.2 // indirect + github.com/jackc/pgx/v5 v5.10.0 // indirect github.com/mujhtech/headgate/go/headgatemigrate v0.1.6 // indirect - github.com/redis/go-redis/v9 v9.7.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/text v0.21.0 // indirect + github.com/redis/go-redis/v9 v9.22.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect ) replace github.com/mujhtech/headgate/go => ../../go diff --git a/examples/go/go.sum b/examples/go/go.sum index 6d21a6d..1193e09 100644 --- a/examples/go/go.sum +++ b/examples/go/go.sum @@ -7,36 +7,37 @@ github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/batch_handler_test.go b/go/batch_handler_test.go index e0362de..bc6bf56 100644 --- a/go/batch_handler_test.go +++ b/go/batch_handler_test.go @@ -6,6 +6,7 @@ import ( "sync" "sync/atomic" "testing" + "testing/synctest" "time" ) @@ -56,26 +57,42 @@ func TestRegisterBatchFuncRunsOneCallAndReturnsPerJobResults(t *testing.T) { } func TestRegisterBatchFuncFlushesAtMaxDelay(t *testing.T) { - reg := NewRegistry() - if err := RegisterBatchFunc[batchArgs](reg, 10, 5*time.Millisecond, func(jobs []BatchJob[batchArgs]) []error { - return make([]error, len(jobs)) - }); err != nil { - t.Fatal(err) - } - done := make(chan error, 1) - go func() { - done <- reg.handlers["batch.test"](context.Background(), Claim{Envelope: Envelope{ - ID: "job", Kind: "batch.test", Payload: []byte(`{"N":1}`), - }}) - }() - select { - case err := <-done: - if err != nil { + synctest.Test(t, func(t *testing.T) { + reg := NewRegistry() + var calls atomic.Int32 + if err := reg.RegisterBatchFunc[batchArgs](10, 5*time.Millisecond, func(jobs []BatchJob[batchArgs]) []error { + calls.Add(1) + if len(jobs) != 1 || jobs[0].Job.Args.N != 1 { + t.Errorf("unexpected batch: %+v", jobs) + } + return make([]error, len(jobs)) + }); err != nil { t.Fatal(err) } - case <-time.After(time.Second): - t.Fatal("batch did not flush at max delay") - } + done := make(chan error, 1) + go func() { + done <- reg.handlers["batch.test"](context.Background(), Claim{Envelope: Envelope{ + ID: "job", Kind: "batch.test", Payload: []byte(`{"N":1}`), + }}) + }() + synctest.Wait() + synctest.Sleep(4 * time.Millisecond) + if calls.Load() != 0 { + t.Fatal("batch flushed before its maximum delay") + } + synctest.Sleep(time.Millisecond) + select { + case err := <-done: + if err != nil { + t.Fatal(err) + } + default: + t.Fatal("batch did not flush at max delay") + } + if calls.Load() != 1 { + t.Fatalf("batch calls = %d, want 1", calls.Load()) + } + }) } func TestCancelledPendingBatchMemberNeverReachesHandler(t *testing.T) { @@ -109,3 +126,26 @@ func TestCancelledPendingBatchMemberNeverReachesHandler(t *testing.T) { default: } } + +func TestRegistryBatchMethodRejectsInvalidConfiguration(t *testing.T) { + for _, tc := range []struct { + name string + size int + delay time.Duration + }{ + {"zero-size", 0, time.Second}, {"sub-millisecond", 1, time.Microsecond}, + } { + t.Run(tc.name, func(t *testing.T) { + reg := NewRegistry() + if err := reg.RegisterBatchFunc[batchArgs](tc.size, tc.delay, func([]BatchJob[batchArgs]) []error { + t.Error("invalid batch handler ran") + return nil + }); err == nil { + t.Fatal("invalid configuration accepted") + } + if len(reg.handlers) != 0 { + t.Fatal("invalid configuration left a handler registered") + } + }) + } +} diff --git a/go/driver/headgatemysql/go.mod b/go/driver/headgatemysql/go.mod index 8c5d41d..0ab867b 100644 --- a/go/driver/headgatemysql/go.mod +++ b/go/driver/headgatemysql/go.mod @@ -1,9 +1,9 @@ module github.com/mujhtech/headgate/go/driver/headgatemysql -go 1.24.0 +go 1.27.0 require ( - github.com/go-sql-driver/mysql v1.9.3 + github.com/go-sql-driver/mysql v1.10.1 github.com/mujhtech/headgate/go v0.1.6 github.com/mujhtech/headgate/go/headgatemigrate v0.1.6 github.com/mujhtech/headgate/go/headgatetest v0.1.6 @@ -12,13 +12,14 @@ require ( require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.2 // indirect - github.com/redis/go-redis/v9 v9.7.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/text v0.21.0 // indirect + github.com/jackc/pgx/v5 v5.10.0 // indirect + github.com/redis/go-redis/v9 v9.22.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect ) replace github.com/mujhtech/headgate/go => ../.. diff --git a/go/driver/headgatemysql/go.sum b/go/driver/headgatemysql/go.sum index 6d21a6d..1193e09 100644 --- a/go/driver/headgatemysql/go.sum +++ b/go/driver/headgatemysql/go.sum @@ -7,36 +7,37 @@ github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/driver/headgatemysql/inspect.go b/go/driver/headgatemysql/inspect.go index f5c2905..0b390b1 100644 --- a/go/driver/headgatemysql/inspect.go +++ b/go/driver/headgatemysql/inspect.go @@ -1665,6 +1665,9 @@ func (s *MysqlStore) SampleQueueMemory(ctx context.Context, limit uint32) (uint3 qs = append(qs, q) } _ = rows.Close() + if err := rows.Err(); err != nil { + return 0, err + } for _, q := range qs { var bytes uint64 var n uint32 diff --git a/go/driver/headgatepgx/go.mod b/go/driver/headgatepgx/go.mod index 85a548f..54dcb4f 100644 --- a/go/driver/headgatepgx/go.mod +++ b/go/driver/headgatepgx/go.mod @@ -1,9 +1,9 @@ module github.com/mujhtech/headgate/go/driver/headgatepgx -go 1.24.0 +go 1.27.0 require ( - github.com/jackc/pgx/v5 v5.7.2 + github.com/jackc/pgx/v5 v5.10.0 github.com/mujhtech/headgate/go v0.1.6 github.com/mujhtech/headgate/go/headgatemigrate v0.1.6 github.com/mujhtech/headgate/go/headgatetest v0.1.6 @@ -12,15 +12,16 @@ require ( require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/go-sql-driver/mysql v1.10.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/redis/go-redis/v9 v9.7.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/text v0.21.0 // indirect + github.com/redis/go-redis/v9 v9.22.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect ) replace github.com/mujhtech/headgate/go => ../.. diff --git a/go/driver/headgatepgx/go.sum b/go/driver/headgatepgx/go.sum index 6d21a6d..1193e09 100644 --- a/go/driver/headgatepgx/go.sum +++ b/go/driver/headgatepgx/go.sum @@ -7,36 +7,37 @@ github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/driver/headgateredis/go.mod b/go/driver/headgateredis/go.mod index e949a1d..a418ded 100644 --- a/go/driver/headgateredis/go.mod +++ b/go/driver/headgateredis/go.mod @@ -1,24 +1,25 @@ module github.com/mujhtech/headgate/go/driver/headgateredis -go 1.24.0 +go 1.27.0 require ( github.com/mujhtech/headgate/go v0.1.6 github.com/mujhtech/headgate/go/headgatetest v0.1.6 - github.com/redis/go-redis/v9 v9.7.0 + github.com/redis/go-redis/v9 v9.22.0 ) require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/go-sql-driver/mysql v1.10.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.2 // indirect + github.com/jackc/pgx/v5 v5.10.0 // indirect github.com/mujhtech/headgate/go/headgatemigrate v0.1.6 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/text v0.21.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect ) replace github.com/mujhtech/headgate/go => ../.. diff --git a/go/driver/headgateredis/go.sum b/go/driver/headgateredis/go.sum index 6d21a6d..1193e09 100644 --- a/go/driver/headgateredis/go.sum +++ b/go/driver/headgateredis/go.sum @@ -7,36 +7,37 @@ github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/driver/headgateredis/store.go b/go/driver/headgateredis/store.go index dbf160c..536e250 100644 --- a/go/driver/headgateredis/store.go +++ b/go/driver/headgateredis/store.go @@ -325,8 +325,7 @@ func (s *RedisStore) AckAttemptWithActualWeight(ctx context.Context, lease headg return err } if e := parseTagged(res); e != nil { - var lr *headgate.LeaseRejectedError - if errors.As(e, &lr) { + if _, ok := errors.AsType[*headgate.LeaseRejectedError](e); ok { return &headgate.LeaseRejectedError{JobID: lease.JobID} } return e @@ -354,8 +353,7 @@ func (s *RedisStore) AckSuccessWithResult(ctx context.Context, lease headgate.Le return err } if err := parseTagged(res); err != nil { - var rejected *headgate.LeaseRejectedError - if errors.As(err, &rejected) { + if _, ok := errors.AsType[*headgate.LeaseRejectedError](err); ok { return &headgate.LeaseRejectedError{JobID: lease.JobID} } return err @@ -377,8 +375,7 @@ func (s *RedisStore) WriteJobOutput( return nil, err } if err := parseTagged(res); err != nil { - var rejected *headgate.LeaseRejectedError - if errors.As(err, &rejected) { + if _, ok := errors.AsType[*headgate.LeaseRejectedError](err); ok { return nil, &headgate.LeaseRejectedError{JobID: lease.JobID} } return nil, err @@ -414,8 +411,7 @@ func (s *RedisStore) WriteJobProgress( return nil, err } if err := parseTagged(res); err != nil { - var rejected *headgate.LeaseRejectedError - if errors.As(err, &rejected) { + if _, ok := errors.AsType[*headgate.LeaseRejectedError](err); ok { return nil, &headgate.LeaseRejectedError{JobID: lease.JobID} } return nil, err @@ -535,8 +531,7 @@ func (s *RedisStore) Checkpoint(ctx context.Context, lease headgate.LeaseRef, cp return err } if e := parseTagged(res); e != nil { - var lr *headgate.LeaseRejectedError - if errors.As(e, &lr) { + if _, ok := errors.AsType[*headgate.LeaseRejectedError](e); ok { return &headgate.LeaseRejectedError{JobID: lease.JobID} } return e diff --git a/go/go.mod b/go/go.mod index 99dc770..27936a2 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,6 +1,6 @@ module github.com/mujhtech/headgate/go -go 1.24.0 +go 1.27.0 // Core has no driver or exporter dependencies. // This is a CI gate, not a guideline: scripts/check-deps.sh fails the build if diff --git a/go/go.work b/go/go.work index 53df636..627cadf 100644 --- a/go/go.work +++ b/go/go.work @@ -1,12 +1,14 @@ -go 1.24.0 +go 1.27.0 + +toolchain go1.27.1 use ( . ./driver/headgatemysql ./driver/headgatepgx ./driver/headgateredis - ./headgatecrypto ./headgateapi + ./headgatecrypto ./headgatectl ./headgatemigrate ./headgateotel diff --git a/go/go.work.sum b/go/go.work.sum index 0ce28eb..bef87b1 100644 --- a/go/go.work.sum +++ b/go/go.work.sum @@ -10,6 +10,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= @@ -45,15 +46,11 @@ golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= -google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= -google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/go/headgate.go b/go/headgate.go index c361113..75944cd 100644 --- a/go/headgate.go +++ b/go/headgate.go @@ -296,12 +296,10 @@ func IsUnavailable(err error) bool { if err == nil { return false } - var una *UnavailableError - if errors.As(err, &una) { + if _, ok := errors.AsType[*UnavailableError](err); ok { return true } - var netErr net.Error - if errors.As(err, &netErr) { + if _, ok := errors.AsType[net.Error](err); ok { return true } return errors.Is(err, syscall.ECONNREFUSED) || @@ -324,8 +322,7 @@ func WrapUnavailable(err error) error { if err == nil { return nil } - var unavailable *UnavailableError - if errors.As(err, &unavailable) { + if _, ok := errors.AsType[*UnavailableError](err); ok { return err } if IsUnavailable(err) { diff --git a/go/headgateapi/api.go b/go/headgateapi/api.go index addfa8d..44b3f8b 100644 --- a/go/headgateapi/api.go +++ b/go/headgateapi/api.go @@ -413,8 +413,7 @@ func (a *api) authorizeEnqueue(w http.ResponseWriter, r *http.Request, batch []h if err == nil { return true } - var forbidden *headgate.EnqueueForbiddenError - if errors.As(err, &forbidden) { + if forbidden, ok := errors.AsType[*headgate.EnqueueForbiddenError](err); ok { writeJSON(w, http.StatusForbidden, map[string]any{ "error": "enqueue forbidden", "kind": forbidden.Kind, }) @@ -498,8 +497,7 @@ func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) (map[string]jso } data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxRequestBody)) if err != nil { - var tooLarge *http.MaxBytesError - if errors.As(err, &tooLarge) { + if _, ok := errors.AsType[*http.MaxBytesError](err); ok { errJSON(w, http.StatusRequestEntityTooLarge, "request body exceeds 2097152 bytes") return nil, false } @@ -507,8 +505,7 @@ func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) (map[string]jso return nil, false } if err := json.Unmarshal(data, dst); err != nil { - var typeErr *json.UnmarshalTypeError - if errors.As(err, &typeErr) { + if _, ok := errors.AsType[*json.UnmarshalTypeError](err); ok { // Valid JSON, wrong shape — serde's `invalid type: …, expected i32` case. errJSON(w, http.StatusUnprocessableEntity, msgBadBody) return nil, false diff --git a/go/headgateapi/api_test.go b/go/headgateapi/api_test.go index e3a3702..84f9b5f 100644 --- a/go/headgateapi/api_test.go +++ b/go/headgateapi/api_test.go @@ -1,6 +1,7 @@ package headgateapi import ( + "bufio" "context" "encoding/json" "errors" @@ -12,11 +13,102 @@ import ( "strings" "syscall" "testing" + "testing/synctest" "time" headgate "github.com/mujhtech/headgate/go" ) +type streamAPIStore struct { + errStore + wake chan string + stopped chan struct{} +} + +func (s *streamAPIStore) Caps() headgate.Caps { return headgate.CapInspect | headgate.CapNotifying } + +func (s *streamAPIStore) WaitWakeup(ctx context.Context, _ []string, _ time.Duration) (string, bool, error) { + select { + case q := <-s.wake: + return q, true, nil + case <-ctx.Done(): + close(s.stopped) + return "", false, ctx.Err() + } +} + +func TestEventStreamHeartbeatCoalescingAndCancellation(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + store := &streamAPIStore{wake: make(chan string, 2), stopped: make(chan struct{})} + finished := make(chan struct{}) + handler := Handler(store) + server := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer close(finished) + handler.ServeHTTP(w, r) + })) + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://control.example/api/v1/events", nil) + if err != nil { + t.Fatal(err) + } + resp, err := server.Client().Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK || resp.Header.Get("Content-Type") != "text/event-stream" { + t.Fatalf("status=%d headers=%v", resp.StatusCode, resp.Header) + } + frames := make(chan string, 4) + readDone := make(chan struct{}) + go func() { + defer close(readDone) + scanner := bufio.NewScanner(resp.Body) + var frame strings.Builder + for scanner.Scan() { + if scanner.Text() == "" { + frames <- frame.String() + frame.Reset() + } else { + frame.WriteString(scanner.Text() + "\n") + } + } + }() + assertNoFrame := func() { + t.Helper() + select { + case frame := <-frames: + t.Fatalf("early frame: %q", frame) + default: + } + } + synctest.Sleep(15*time.Second - time.Nanosecond) + assertNoFrame() + synctest.Sleep(time.Nanosecond) + if frame := <-frames; frame != ": hb\n" { + t.Fatalf("heartbeat = %q", frame) + } + store.wake <- "critical" + store.wake <- "critical" + synctest.Wait() + synctest.Sleep(200*time.Millisecond - time.Nanosecond) + assertNoFrame() + synctest.Sleep(time.Nanosecond) + if frame := <-frames; frame != "event: queue_activity\ndata: {\"queues\":[\"critical\"]}\n" { + t.Fatalf("coalesced frame = %q", frame) + } + // Disconnect while a fresh coalescing timer and a store wait are active. + store.wake <- "default" + synctest.Wait() + cancel() + <-store.stopped + <-finished + <-readDone + assertNoFrame() + }) +} + // errStore answers every Inspect call the API can make with one canned error. It // exists to exercise the arms of storeErr that need a BROKEN store — the ones no // conformance run can reach while Postgres is up, and which therefore went four rounds diff --git a/go/headgateapi/cmd/hg-go-api/main.go b/go/headgateapi/cmd/hg-go-api/main.go index 9ff4c52..6490088 100644 --- a/go/headgateapi/cmd/hg-go-api/main.go +++ b/go/headgateapi/cmd/hg-go-api/main.go @@ -99,5 +99,10 @@ func main() { api.ServeHTTP(w, r) }) log.Printf("hg-go-api (%s) listening on %s — console at http://%s/admin", backend, addr, addr) - log.Fatal(http.ListenAndServe(addr, root)) + server := &http.Server{ + Addr: addr, Handler: root, + // Bound header fan-out separately from net/http's byte limit. + MaxHeaderValueCount: 128, + } + log.Fatal(server.ListenAndServe()) } diff --git a/go/headgateapi/go.mod b/go/headgateapi/go.mod index 62e5efb..66670eb 100644 --- a/go/headgateapi/go.mod +++ b/go/headgateapi/go.mod @@ -1,32 +1,30 @@ module github.com/mujhtech/headgate/go/headgateapi -go 1.24.0 - -require github.com/mujhtech/headgate/go v0.1.6 +go 1.27.0 replace github.com/mujhtech/headgate/go => ../ -require github.com/mujhtech/headgate/go/driver/headgatepgx v0.1.6 - -require github.com/mujhtech/headgate/go/driver/headgateredis v0.1.6 - -require github.com/mujhtech/headgate/go/driver/headgatemysql v0.1.6 - -require github.com/mujhtech/headgate/go/headgateui v0.1.6 +require ( + github.com/mujhtech/headgate/go v0.1.6 + github.com/mujhtech/headgate/go/driver/headgatemysql v0.1.6 + github.com/mujhtech/headgate/go/driver/headgatepgx v0.1.6 + github.com/mujhtech/headgate/go/driver/headgateredis v0.1.6 + github.com/mujhtech/headgate/go/headgateui v0.1.6 +) require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/go-sql-driver/mysql v1.10.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.2 // indirect + github.com/jackc/pgx/v5 v5.10.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/redis/go-redis/v9 v9.7.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/text v0.21.0 // indirect + github.com/redis/go-redis/v9 v9.22.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect ) replace github.com/mujhtech/headgate/go/driver/headgatepgx => ../driver/headgatepgx diff --git a/go/headgateapi/go.sum b/go/headgateapi/go.sum index 675643b..a06bcef 100644 --- a/go/headgateapi/go.sum +++ b/go/headgateapi/go.sum @@ -1,36 +1,49 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/mujhtech/headgate/go/headgatemigrate v0.1.6 h1:hKHm2SS/U5yc5gqGGj+DGyvdCGnN2tDPLGZ/rgIB1Y8= +github.com/mujhtech/headgate/go/headgatemigrate v0.1.6/go.mod h1:M283ByCc7g/6/HmZC95SKB2HL1KRM5i/kYOon5FowZI= +github.com/mujhtech/headgate/go/headgatetest v0.1.6 h1:zd3sPFcHjSdv5VxeVirJBieGwAqgTekT8NzFMU6EQlI= +github.com/mujhtech/headgate/go/headgatetest v0.1.6/go.mod h1:AGOIN4nrekxBOvlcnVcXnjx2dxG5Oh3WHJAdWjHwOpI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go/headgatecrypto/go.mod b/go/headgatecrypto/go.mod index 0076db0..d8d94bb 100644 --- a/go/headgatecrypto/go.mod +++ b/go/headgatecrypto/go.mod @@ -1,10 +1,14 @@ module github.com/mujhtech/headgate/go/headgatecrypto -go 1.24.0 +go 1.27.0 require github.com/mujhtech/headgate/go v0.1.6 -require github.com/mujhtech/headgate/go/headgatetest v0.1.6 // indirect +require ( + github.com/mujhtech/headgate/go/headgatetest v0.1.6 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/sys v0.47.0 // indirect +) replace github.com/mujhtech/headgate/go => .. diff --git a/go/headgatecrypto/go.sum b/go/headgatecrypto/go.sum index 8dcd754..4210042 100644 --- a/go/headgatecrypto/go.sum +++ b/go/headgatecrypto/go.sum @@ -2,21 +2,21 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= -github.com/mujhtech/headgate/go/headgatemigrate v0.1.1 h1:Yy1Xy2azzMqUpTQ48bAx1vPPo8qIN/obEmFrKhLyc6k= -github.com/mujhtech/headgate/go/headgatemigrate v0.1.1/go.mod h1:ER1AEEvznD9xvbZW6S34MwvyJkyp9NWGABBXLyKSHH8= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/mujhtech/headgate/go/headgatemigrate v0.1.6 h1:hKHm2SS/U5yc5gqGGj+DGyvdCGnN2tDPLGZ/rgIB1Y8= +github.com/mujhtech/headgate/go/headgatemigrate v0.1.6/go.mod h1:M283ByCc7g/6/HmZC95SKB2HL1KRM5i/kYOon5FowZI= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= diff --git a/go/headgatectl/go.mod b/go/headgatectl/go.mod index cc376ec..a4b8d0d 100644 --- a/go/headgatectl/go.mod +++ b/go/headgatectl/go.mod @@ -1,10 +1,10 @@ module github.com/mujhtech/headgate/go/headgatectl -go 1.24 +go 1.27.0 -require github.com/spf13/cobra v1.10.1 +require github.com/spf13/cobra v1.10.2 require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/spf13/pflag v1.0.10 // indirect ) diff --git a/go/headgatectl/go.sum b/go/headgatectl/go.sum index e613680..ef5d78d 100644 --- a/go/headgatectl/go.sum +++ b/go/headgatectl/go.sum @@ -2,9 +2,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/headgatectl/main_test.go b/go/headgatectl/main_test.go index f2975ad..2ec4d2f 100644 --- a/go/headgatectl/main_test.go +++ b/go/headgatectl/main_test.go @@ -1,10 +1,15 @@ package main import ( + "context" + "errors" "io" "net/http" + "net/http/httptest" "strings" "testing" + "testing/synctest" + "time" ) type roundTripFunc func(*http.Request) (*http.Response, error) @@ -23,3 +28,46 @@ func TestClientUsesBoundedControlAPIAndBearerAuthentication(t *testing.T) { t.Fatal(err) } } + +func TestClientTimeoutCancelsRequestAndBodyRead(t *testing.T) { + for _, streaming := range []bool{false, true} { + name := "waiting-for-headers" + if streaming { + name = "waiting-for-body" + } + t.Run(name, func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + started, stopped := make(chan struct{}), make(chan struct{}) + server := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer close(stopped) + if streaming { + w.WriteHeader(http.StatusOK) + if err := http.NewResponseController(w).Flush(); err != nil { + t.Error(err) + return + } + } + close(started) + <-r.Context().Done() + })) + httpClient := server.Client() + httpClient.Timeout = 30 * time.Second + c := client{base: "http://control.example", http: httpClient} + result := make(chan error, 1) + go func() { result <- c.call(http.MethodGet, "/queues", nil) }() + <-started + synctest.Sleep(30*time.Second - time.Nanosecond) + select { + case err := <-result: + t.Fatalf("request ended before timeout: %v", err) + default: + } + synctest.Sleep(time.Nanosecond) + if err := <-result; !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("timeout = %v, want DeadlineExceeded", err) + } + <-stopped + }) + }) + } +} diff --git a/go/headgatemigrate/go.mod b/go/headgatemigrate/go.mod index 09a0bff..4650c7e 100644 --- a/go/headgatemigrate/go.mod +++ b/go/headgatemigrate/go.mod @@ -1,10 +1,10 @@ module github.com/mujhtech/headgate/go/headgatemigrate -go 1.24.0 +go 1.27.0 require ( - github.com/go-sql-driver/mysql v1.9.3 - github.com/jackc/pgx/v5 v5.7.2 + github.com/go-sql-driver/mysql v1.10.1 + github.com/jackc/pgx/v5 v5.10.0 github.com/mujhtech/headgate/go v0.1.6 ) @@ -12,9 +12,8 @@ require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/stretchr/testify v1.11.1 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/text v0.21.0 // indirect + github.com/stretchr/testify v1.12.1 // indirect + golang.org/x/text v0.41.0 // indirect ) replace github.com/mujhtech/headgate/go => .. diff --git a/go/headgatemigrate/go.sum b/go/headgatemigrate/go.sum index 7269cd3..4ce41f9 100644 --- a/go/headgatemigrate/go.sum +++ b/go/headgatemigrate/go.sum @@ -1,13 +1,27 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/headgateotel/go.mod b/go/headgateotel/go.mod index b5874e4..4f90e59 100644 --- a/go/headgateotel/go.mod +++ b/go/headgateotel/go.mod @@ -1,14 +1,14 @@ module github.com/mujhtech/headgate/go/headgateotel -go 1.24.0 +go 1.27.0 require ( github.com/mujhtech/headgate/go v0.1.6 - go.opentelemetry.io/otel v1.41.0 - go.opentelemetry.io/otel/metric v1.41.0 - go.opentelemetry.io/otel/sdk v1.41.0 - go.opentelemetry.io/otel/sdk/metric v1.41.0 - go.opentelemetry.io/otel/trace v1.41.0 + go.opentelemetry.io/otel v1.46.0 + go.opentelemetry.io/otel/metric v1.46.0 + go.opentelemetry.io/otel/sdk v1.46.0 + go.opentelemetry.io/otel/sdk/metric v1.46.0 + go.opentelemetry.io/otel/trace v1.46.0 ) require ( @@ -18,10 +18,10 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/mujhtech/headgate/go/headgatetest v0.1.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/sys v0.47.0 // indirect ) -require golang.org/x/sys v0.41.0 // indirect - replace github.com/mujhtech/headgate/go => .. replace github.com/mujhtech/headgate/go/headgatemigrate => ../headgatemigrate diff --git a/go/headgateotel/go.sum b/go/headgateotel/go.sum index 3f86281..3ae07dc 100644 --- a/go/headgateotel/go.sum +++ b/go/headgateotel/go.sum @@ -2,17 +2,13 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -21,33 +17,33 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= +go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= +go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU= +go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4= +go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= +go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= +go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= diff --git a/go/headgateshared/json_v2_test.go b/go/headgateshared/json_v2_test.go new file mode 100644 index 0000000..50a0227 --- /dev/null +++ b/go/headgateshared/json_v2_test.go @@ -0,0 +1,93 @@ +package headgateshared + +import ( + jsonv1 "encoding/json" + jsonv2 "encoding/json/v2" + "reflect" + "testing" +) + +// These are wire contracts, not an invitation to switch defaults. A direct v2 +// migration can change fingerprints even when the decoded values look equivalent. +func TestJSONV2CompatibilityPreservesWireBytes(t *testing.T) { + for _, tc := range []struct { + name string + value any + want string + }{ + {"nil-list", []string(nil), `null`}, + {"empty-list", []string{}, `[]`}, + {"map-order-and-html", map[string]string{"z": "", "a": "first"}, `{"a":"first","z":"\u003ctag\u003e"}`}, + {"omit-zero", struct { + Count int `json:"count,omitempty"` + }{}, `{}`}, + {"invalid-utf8", string([]byte{0xff}), `"�"`}, + } { + t.Run(tc.name, func(t *testing.T) { + old, err := jsonv1.Marshal(tc.value) + if err != nil { + t.Fatal(err) + } + compatible, err := jsonv2.Marshal(tc.value, jsonv1.DefaultOptionsV1()) + if err != nil { + t.Fatal(err) + } + if string(old) != tc.want || string(compatible) != tc.want { + t.Fatalf("v1=%s compatible-v2=%s want=%s", old, compatible, tc.want) + } + }) + } +} + +func TestJSONV2DefaultsChangeExistingContracts(t *testing.T) { + t.Run("nil-list", func(t *testing.T) { + encoded, err := jsonv2.Marshal([]string(nil)) + if err != nil || string(encoded) != `[]` { + t.Fatalf("v2 nil list = %s, %v", encoded, err) + } + }) + t.Run("omit-zero", func(t *testing.T) { + encoded, err := jsonv2.Marshal(struct { + Count int `json:"count,omitempty"` + }{}) + if err != nil || string(encoded) != `{"count":0}` { + t.Fatalf("v2 zero = %s, %v", encoded, err) + } + }) + t.Run("invalid-utf8", func(t *testing.T) { + if _, err := jsonv2.Marshal(string([]byte{0xff})); err == nil { + t.Fatal("v2 accepted invalid UTF-8") + } + }) + t.Run("duplicate-keys", func(t *testing.T) { + var old, compatible, strict map[string]int + input := []byte(`{"value":1,"value":2}`) + if err := jsonv1.Unmarshal(input, &old); err != nil { + t.Fatal(err) + } + if err := jsonv2.Unmarshal(input, &compatible, jsonv1.DefaultOptionsV1()); err != nil { + t.Fatal(err) + } + if old["value"] != 2 || !reflect.DeepEqual(old, compatible) { + t.Fatalf("v1=%v compatible=%v", old, compatible) + } + if err := jsonv2.Unmarshal(input, &strict); err == nil { + t.Fatal("v2 accepted duplicate names") + } + }) + t.Run("field-case", func(t *testing.T) { + type payload struct { + Value int `json:"value"` + } + var old, strict payload + if err := jsonv1.Unmarshal([]byte(`{"VALUE":42}`), &old); err != nil { + t.Fatal(err) + } + if err := jsonv2.Unmarshal([]byte(`{"VALUE":42}`), &strict); err != nil { + t.Fatal(err) + } + if old.Value != 42 || strict.Value != 0 { + t.Fatalf("v1=%+v v2=%+v", old, strict) + } + }) +} diff --git a/go/headgatetest/go.mod b/go/headgatetest/go.mod index a157a31..6dbb1c6 100644 --- a/go/headgatetest/go.mod +++ b/go/headgatetest/go.mod @@ -1,23 +1,24 @@ module github.com/mujhtech/headgate/go/headgatetest -go 1.24.0 +go 1.27.0 require ( - github.com/go-sql-driver/mysql v1.9.3 - github.com/jackc/pgx/v5 v5.7.2 + github.com/go-sql-driver/mysql v1.10.1 + github.com/jackc/pgx/v5 v5.10.0 github.com/mujhtech/headgate/go v0.1.6 github.com/mujhtech/headgate/go/headgatemigrate v0.1.6 - github.com/redis/go-redis/v9 v9.7.0 + github.com/redis/go-redis/v9 v9.22.0 ) require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/text v0.21.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect ) replace github.com/mujhtech/headgate/go => .. diff --git a/go/headgatetest/go.sum b/go/headgatetest/go.sum index bdd6291..1193e09 100644 --- a/go/headgatetest/go.sum +++ b/go/headgatetest/go.sum @@ -1,16 +1,43 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/headgateui/go.mod b/go/headgateui/go.mod index 919457b..30a3a57 100644 --- a/go/headgateui/go.mod +++ b/go/headgateui/go.mod @@ -1,3 +1,3 @@ module github.com/mujhtech/headgate/go/headgateui -go 1.24 +go 1.27.0 diff --git a/go/headgateworkflow/go.mod b/go/headgateworkflow/go.mod index 469f48e..fad1215 100644 --- a/go/headgateworkflow/go.mod +++ b/go/headgateworkflow/go.mod @@ -1,6 +1,6 @@ module github.com/mujhtech/headgate/go/headgateworkflow -go 1.24.0 +go 1.27.0 require github.com/mujhtech/headgate/go v0.1.6 diff --git a/go/headgateworkflow/go.sum b/go/headgateworkflow/go.sum index 8dcd754..4210042 100644 --- a/go/headgateworkflow/go.sum +++ b/go/headgateworkflow/go.sum @@ -2,21 +2,21 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= -github.com/mujhtech/headgate/go/headgatemigrate v0.1.1 h1:Yy1Xy2azzMqUpTQ48bAx1vPPo8qIN/obEmFrKhLyc6k= -github.com/mujhtech/headgate/go/headgatemigrate v0.1.1/go.mod h1:ER1AEEvznD9xvbZW6S34MwvyJkyp9NWGABBXLyKSHH8= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/mujhtech/headgate/go/headgatemigrate v0.1.6 h1:hKHm2SS/U5yc5gqGGj+DGyvdCGnN2tDPLGZ/rgIB1Y8= +github.com/mujhtech/headgate/go/headgatemigrate v0.1.6/go.mod h1:M283ByCc7g/6/HmZC95SKB2HL1KRM5i/kYOon5FowZI= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= diff --git a/go/headgateworkflow/workflow.go b/go/headgateworkflow/workflow.go index 1ce000b..8b2333e 100644 --- a/go/headgateworkflow/workflow.go +++ b/go/headgateworkflow/workflow.go @@ -224,10 +224,8 @@ func tick(ctx context.Context, inspect headgate.InspectStore, workflow Coordinat results := make(chan readResult, len(workflow.Nodes)) workers := min(workflowWorkers, len(workflow.Nodes)) var reads sync.WaitGroup - reads.Add(workers) for range workers { - go func() { - defer reads.Done() + reads.Go(func() { for node := range work { job, err := inspect.GetJob(readCtx, node.JobID, false) select { @@ -240,7 +238,7 @@ func tick(ctx context.Context, inspect headgate.InspectStore, workflow Coordinat return } } - }() + }) } go func() { defer close(work) @@ -293,10 +291,8 @@ func tick(ctx context.Context, inspect headgate.InspectStore, workflow Coordinat defer cancelMutations() workers = min(workflowWorkers, len(mutations)) var writes sync.WaitGroup - writes.Add(workers) for range workers { - go func() { - defer writes.Done() + writes.Go(func() { for mutation := range mutationWork { var err error if mutation.delete { @@ -310,7 +306,7 @@ func tick(ctx context.Context, inspect headgate.InspectStore, workflow Coordinat return } } - }() + }) } go func() { defer close(mutationWork) diff --git a/go/insert_hook.go b/go/insert_hook.go index 7e3fe4c..8774c11 100644 --- a/go/insert_hook.go +++ b/go/insert_hook.go @@ -49,14 +49,12 @@ func classifyInsertOutcome(err error) InsertOutcome { if err == nil { return InsertOutcome{Kind: InsertOutcomeSucceeded} } - var duplicate *DuplicateError - if errors.As(err, &duplicate) { + if duplicate, ok := errors.AsType[*DuplicateError](err); ok { return InsertOutcome{ Kind: InsertOutcomeDuplicate, ExistingID: duplicate.ExistingID, Replaced: duplicate.Replaced, Err: err, } } - var conflict *IDConflictError - if errors.As(err, &conflict) { + if conflict, ok := errors.AsType[*IDConflictError](err); ok { return InsertOutcome{ Kind: InsertOutcomeIDConflict, JobID: conflict.JobID, Err: err, } diff --git a/go/runtime.go b/go/runtime.go index b6ac0f2..266a1d4 100644 --- a/go/runtime.go +++ b/go/runtime.go @@ -17,6 +17,7 @@ import ( "log/slog" "math/rand/v2" "os" + "runtime/pprof" "sort" "strconv" "sync" @@ -64,6 +65,21 @@ type Registry struct { func NewRegistry() *Registry { return &Registry{handlers: map[string]erasedHandler{}} } +// RegisterWorker registers a typed worker using the same validation as RegisterWorker. +func (r *Registry) RegisterWorker[T Args](worker Worker[T]) error { + return RegisterWorker[T](r, worker) +} + +// RegisterFunc registers a typed handler for T's kind and aliases. +func (r *Registry) RegisterFunc[T Args](work func(context.Context, *Job[T]) error) error { + return RegisterFunc[T](r, work) +} + +// RegisterBatchFunc registers a typed chunk handler with positional member results. +func (r *Registry) RegisterBatchFunc[T Args](maxSize int, maxDelay time.Duration, work func([]BatchJob[T]) []error) error { + return RegisterBatchFunc[T](r, maxSize, maxDelay, work) +} + // RegisterWorker registers w for T's kind and aliases. Payloads decode via the default // JSON codec (payload codecs); a Versioned T gets its Upcast called for foreign schema versions. func RegisterWorker[T Args](r *Registry, w Worker[T]) error { @@ -465,6 +481,14 @@ type inflightJob struct { // Run until Shutdown() (or ctx cancellation). Store outages degrade to backoff-and- // retry, never a crash of the loop. func (r *Runner) Run(ctx context.Context) error { + var err error + pprof.Do(ctx, pprof.Labels("headgate.worker", r.workerID, "headgate.role", "worker"), func(ctx context.Context) { + err = r.run(ctx) + }) + return err +} + +func (r *Runner) run(ctx context.Context) error { heartbeatEvery := r.cfg.LeaseDuration / 3 if heartbeatEvery < 10*time.Millisecond { heartbeatEvery = 10 * time.Millisecond @@ -832,6 +856,10 @@ func (r *Runner) drain(ctx context.Context, mu *sync.Mutex, inflight map[string] func (r *Runner) dutyLoop(ctx context.Context, duty string, dutyStop <-chan struct{}, wg *sync.WaitGroup) { defer wg.Done() + parent := ctx + ctx = pprof.WithLabels(ctx, pprof.Labels("headgate.role", "duty", "headgate.duty", duty)) + pprof.SetGoroutineLabels(ctx) + defer pprof.SetGoroutineLabels(parent) for { select { case <-ctx.Done(): @@ -930,6 +958,19 @@ func (r *Runner) runDuty(ctx context.Context, duty string) { // had nothing behind it: a helper that runs one job but cannot say what happened to it is // Drain with extra steps. func (r *Runner) processOne(ctx context.Context, claim Claim, steps *stepState) string { + var outcome string + // Stable operational metadata identifies blocked work without copying payloads, + // tenant keys, headers, or per-job IDs into profiles and crash tracebacks. + pprof.Do(ctx, pprof.Labels( + "headgate.worker", r.workerID, "headgate.role", "job", + "headgate.queue", claim.Envelope.Queue, "headgate.kind", claim.Envelope.Kind, + ), func(ctx context.Context) { + outcome = r.processClaim(ctx, claim, steps) + }) + return outcome +} + +func (r *Runner) processClaim(ctx context.Context, claim Claim, steps *stepState) string { // A fresh job map for EVERY invocation. The worker map is shared deliberately; the // job map is not, which makes two concurrent jobs storing the same T independent. ctx = withTaskData(ctx, r.cfg.Extensions) diff --git a/go/runtime_benchmark_test.go b/go/runtime_benchmark_test.go index 5ecc5c9..9d4a25c 100644 --- a/go/runtime_benchmark_test.go +++ b/go/runtime_benchmark_test.go @@ -2,9 +2,70 @@ package headgate import ( "context" + "encoding/json" + "strings" "testing" ) +type benchmarkArgs struct { + Account string `json:"account"` + Message string `json:"message"` + Attempt int `json:"attempt"` +} + +func (benchmarkArgs) Kind() string { return "benchmark.delivery" } + +func benchmarkEnvelope(b *testing.B) Envelope { + b.Helper() + payload, err := json.Marshal(benchmarkArgs{Account: "account-42", Message: strings.Repeat("x", 1024), Attempt: 3}) + if err != nil { + b.Fatal(err) + } + return Envelope{ID: "bench-job", Kind: "benchmark.delivery", Queue: "default", Payload: payload} +} + +func BenchmarkDecodeArgs1K(b *testing.B) { + envelope := benchmarkEnvelope(b) + b.ReportAllocs() + for b.Loop() { + args, err := DecodeArgs[benchmarkArgs](envelope) + if err != nil || args.Attempt != 3 { + b.Fatalf("decode = %+v, %v", args, err) + } + } +} + +func BenchmarkTypedDispatch1K(b *testing.B) { + registry := NewRegistry() + if err := registry.RegisterFunc(func(_ context.Context, job *Job[benchmarkArgs]) error { + if job.Args.Attempt != 3 { + b.Fatalf("attempt = %d", job.Args.Attempt) + } + return nil + }); err != nil { + b.Fatal(err) + } + claim := Claim{Envelope: benchmarkEnvelope(b)} + handler := registry.handlers[claim.Envelope.Kind] + ctx := context.Background() + b.ReportAllocs() + for b.Loop() { + if err := handler(ctx, claim); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkMarshalArgs1K(b *testing.B) { + args := benchmarkArgs{Account: "account-42", Message: strings.Repeat("x", 1024), Attempt: 3} + b.ReportAllocs() + for b.Loop() { + if _, err := json.Marshal(args); err != nil { + b.Fatal(err) + } + } +} + func BenchmarkValidateEnqueue1KPayload(b *testing.B) { batch := []Envelope{{ ID: "bench-job", Kind: "bench:job", Queue: "default", Payload: make([]byte, 1024), diff --git a/go/runtime_test.go b/go/runtime_test.go index abd6070..ea722d7 100644 --- a/go/runtime_test.go +++ b/go/runtime_test.go @@ -15,14 +15,18 @@ package headgate // asserted here is that a non-zero count reaches the telemetry and trace context facade instead of the floor. import ( + "bytes" "context" "encoding/json" "errors" "fmt" "io" "os" + "runtime/pprof" + "strings" "sync" "testing" + "testing/synctest" "time" ) @@ -173,90 +177,95 @@ func (c *captureTelemetry) eventsOf(typ string) []Event { } func TestMemoryGuardEmitsSampleAndRequestsBoundedRestartAtLimit(t *testing.T) { - cap := &captureTelemetry{} - r := NewRunner(&evictStub{}, NewRegistry(), Config{ - DisableDuties: true, - MemoryLimitBytes: 100, - MemoryCheckInterval: time.Millisecond, - MemorySampler: MemorySamplerFunc(func() (uint64, error) { - return 125, nil - }), - Telemetry: cap, - }) - done := make(chan error, 1) - go func() { done <- r.Run(context.Background()) }() - select { - case err := <-done: - if err != nil { - t.Fatal(err) + synctest.Test(t, func(t *testing.T) { + cap := &captureTelemetry{} + r := NewRunner(&evictStub{}, NewRegistry(), Config{ + DisableDuties: true, + MemoryLimitBytes: 100, + MemoryCheckInterval: time.Millisecond, + MemorySampler: MemorySamplerFunc(func() (uint64, error) { + return 125, nil + }), + Telemetry: cap, + }) + done := make(chan error, 1) + go func() { done <- r.Run(context.Background()) }() + select { + case err := <-done: + if err != nil { + t.Fatal(err) + } + case <-time.After(time.Second): + t.Fatal("memory guard did not stop admission and begin shutdown") } - case <-time.After(time.Second): - t.Fatal("memory guard did not stop admission and begin shutdown") - } - events := cap.eventsOf("worker_memory") - if len(events) != 1 { - t.Fatalf("got %d memory samples, want the threshold sample", len(events)) - } - if got := events[0]; got.MemoryBytes != 125 || got.MemoryLimitBytes != 100 || !got.RestartRequested { - t.Fatalf("threshold telemetry = %+v", got) - } + events := cap.eventsOf("worker_memory") + if len(events) != 1 { + t.Fatalf("got %d memory samples, want the threshold sample", len(events)) + } + if got := events[0]; got.MemoryBytes != 125 || got.MemoryLimitBytes != 100 || !got.RestartRequested { + t.Fatalf("threshold telemetry = %+v", got) + } + }) } func TestMemoryGuardSamplesBelowLimitWithoutStoppingWorker(t *testing.T) { - cap := &captureTelemetry{} - r := NewRunner(&evictStub{}, NewRegistry(), Config{ - DisableDuties: true, - MemoryLimitBytes: 100, - MemoryCheckInterval: time.Millisecond, - MemorySampler: MemorySamplerFunc(func() (uint64, error) { - return 75, nil - }), - Telemetry: cap, + synctest.Test(t, func(t *testing.T) { + cap := &captureTelemetry{} + r := NewRunner(&evictStub{}, NewRegistry(), Config{ + DisableDuties: true, + MemoryLimitBytes: 100, + MemoryCheckInterval: time.Millisecond, + MemorySampler: MemorySamplerFunc(func() (uint64, error) { + return 75, nil + }), + Telemetry: cap, + }) + done := make(chan error, 1) + go func() { done <- r.Run(context.Background()) }() + synctest.Wait() + synctest.Sleep(time.Millisecond) + select { + case <-done: + t.Fatal("a below-limit memory sample stopped the worker") + default: + } + r.Shutdown() + if err := <-done; err != nil { + t.Fatal(err) + } + events := cap.eventsOf("worker_memory") + if len(events) == 0 || events[0].RestartRequested { + t.Fatalf("below-limit telemetry = %+v", events) + } }) - done := make(chan error, 1) - go func() { done <- r.Run(context.Background()) }() - deadline := time.Now().Add(time.Second) - for len(cap.eventsOf("worker_memory")) == 0 && time.Now().Before(deadline) { - time.Sleep(time.Millisecond) - } - select { - case <-done: - t.Fatal("a below-limit memory sample stopped the worker") - default: - } - r.Shutdown() - if err := <-done; err != nil { - t.Fatal(err) - } - events := cap.eventsOf("worker_memory") - if len(events) == 0 || events[0].RestartRequested { - t.Fatalf("below-limit telemetry = %+v", events) - } } func TestRollingRestartDrainIgnoresOrdinaryShutdownTimeout(t *testing.T) { - r := NewRunner(&evictStub{}, NewRegistry(), Config{ShutdownTimeout: time.Millisecond}) - var mu sync.Mutex - inflight := map[string]*inflightJob{} - var wg sync.WaitGroup - wg.Add(1) - done := make(chan struct{}) - go func() { - r.drain(context.Background(), &mu, inflight, &wg, true) - close(done) - }() - time.Sleep(10 * time.Millisecond) - select { - case <-done: - t.Fatal("rolling restart returned at the ordinary shutdown timeout") - default: - } - wg.Done() - select { - case <-done: - case <-time.After(time.Second): - t.Fatal("rolling restart did not finish after in-flight work completed") - } + synctest.Test(t, func(t *testing.T) { + r := NewRunner(&evictStub{}, NewRegistry(), Config{ShutdownTimeout: time.Millisecond}) + var mu sync.Mutex + inflight := map[string]*inflightJob{} + var wg sync.WaitGroup + wg.Add(1) + done := make(chan struct{}) + go func() { + r.drain(context.Background(), &mu, inflight, &wg, true) + close(done) + }() + synctest.Wait() + synctest.Sleep(10 * time.Millisecond) + select { + case <-done: + t.Fatal("rolling restart returned at the ordinary shutdown timeout") + default: + } + wg.Done() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("rolling restart did not finish after in-flight work completed") + } + }) } func TestRetentionSweepIsNeverSilent(t *testing.T) { @@ -351,6 +360,221 @@ type rjArgs struct{} func (rjArgs) Kind() string { return "rj" } +type registryMethodArgs struct{ Value int } + +func (registryMethodArgs) Kind() string { return "profile.test" } +func (registryMethodArgs) KindAliases() []string { return []string{"profile.old"} } + +type registryMethodWorker struct { + work func(context.Context, *Job[registryMethodArgs]) error +} + +func (w registryMethodWorker) Work(ctx context.Context, job *Job[registryMethodArgs]) error { + return w.work(ctx, job) +} + +func TestRegistryMethodsPreserveValidationAndDispatch(t *testing.T) { + for _, name := range []string{"function", "worker"} { + t.Run(name, func(t *testing.T) { + reg := NewRegistry() + calls := 0 + handler := func(_ context.Context, job *Job[registryMethodArgs]) error { + calls++ + if job.Args.Value != 42 || job.ID != "method-job" { + t.Errorf("decoded job = %+v", job) + } + return nil + } + var err error + if name == "worker" { + err = reg.RegisterWorker[registryMethodArgs](registryMethodWorker{handler}) + } else { + err = reg.RegisterFunc(handler) // Infer T from the typed callback. + } + if err != nil { + t.Fatal(err) + } + for _, kind := range []string{"profile.test", "profile.old"} { + if err := reg.handlers[kind](context.Background(), Claim{Envelope: Envelope{ + ID: "method-job", Kind: kind, Payload: []byte(`{"Value":42}`), + }}); err != nil { + t.Fatal(err) + } + } + if calls != 2 { + t.Fatalf("calls = %d", calls) + } + if err := RegisterFunc(reg, handler); err == nil { + t.Fatal("package registration did not see method registration") + } + if err := reg.RegisterFunc[vkBadAlias](func(context.Context, *Job[vkBadAlias]) error { return nil }); err == nil { + t.Fatal("invalid alias was accepted") + } + if _, ok := reg.handlers["fine:kind"]; ok { + t.Fatal("failed registration was not atomic") + } + if err := reg.handlers["profile.test"](context.Background(), Claim{Envelope: Envelope{Payload: []byte(`{`)}}); err == nil { + t.Fatal("malformed payload reached handler") + } + if calls != 2 { + t.Fatal("invalid payload caused a side effect") + } + }) + } +} + +func TestWorkerProfileLabelsFollowDispatchAndRestoreCaller(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + reg := NewRegistry() + store := &ackStub{} + runner := NewRunner(store, reg, Config{DisableDuties: true}) + ready := make(chan struct{}) + release := make(chan struct{}) + defer close(release) + checkLabels := func(ctx context.Context) { + for key, want := range map[string]string{ + "headgate.worker": runner.workerID, "headgate.role": "job", + "headgate.queue": "profile-queue", "headgate.kind": "profile.test", "application": "preserved", + } { + if got, ok := pprof.Label(ctx, key); !ok || got != want { + t.Errorf("label %s = %q, %v; want %q", key, got, ok, want) + } + } + } + if err := reg.RegisterFunc[registryMethodArgs](func(ctx context.Context, _ *Job[registryMethodArgs]) error { + checkLabels(ctx) + return Track(ctx, func(ctx context.Context) error { + checkLabels(ctx) + close(ready) + select { + case <-release: + case <-ctx.Done(): + return ctx.Err() + } + return nil + }) + }); err != nil { + t.Fatal(err) + } + claim := Claim{Envelope: Envelope{ID: "private-job-marker", Kind: "profile.test", Queue: "profile-queue", + Payload: []byte(`{"Value":42}`), PartitionKey: "private-tenant-marker"}} + done := make(chan string, 1) + go func() { + pprof.Do(ctx, pprof.Labels("application", "preserved"), func(ctx context.Context) { + steps := newStepState(store, claim) + outcome := runner.processOne(withStepState(ctx, steps), claim, steps) + // Inspect this still-running caller: ending the goroutine would hide + // a missing label restoration just as effectively as restoring it. + var restored bytes.Buffer + if err := pprof.Lookup("goroutine").WriteTo(&restored, 1); err != nil { + t.Error(err) + } + if strings.Contains(restored.String(), `"headgate.kind":"profile.test"`) { + t.Error("dispatch did not restore caller labels") + } + if !strings.Contains(restored.String(), `"application":"preserved"`) { + t.Error("dispatch dropped caller labels") + } + done <- outcome + }) + }() + select { + case <-ready: + case <-ctx.Done(): + t.Fatal("tracked handler did not start") + } + var profile bytes.Buffer + if err := pprof.Lookup("goroutine").WriteTo(&profile, 1); err != nil { + t.Fatal(err) + } + if !strings.Contains(profile.String(), `"headgate.kind":"profile.test"`) { + t.Fatalf("profile is missing job labels: %s", profile.String()) + } + for _, private := range []string{"private-job-marker", "private-tenant-marker"} { + if strings.Contains(profile.String(), private) { + t.Fatalf("profile leaked %s", private) + } + } + // Cancellation also proves labels leave the handler's cancellation context intact. + cancel() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("dispatch ignored cancellation") + } + profile.Reset() + if err := pprof.Lookup("goroutine").WriteTo(&profile, 1); err != nil { + t.Fatal(err) + } + if strings.Contains(profile.String(), `"headgate.kind":"profile.test"`) { + t.Fatal("job labels survived dispatch") + } + if pprof.Lookup("goroutineleak") == nil { + t.Fatal("Go 1.27 leak profile is unavailable") + } +} + +type profileLoopStore struct { + evictStub + worker string + seen chan string + t *testing.T +} + +func (s *profileLoopStore) check(ctx context.Context, role string) { + if got, _ := pprof.Label(ctx, "headgate.worker"); got != s.worker { + s.t.Errorf("worker label = %q", got) + } + if got, _ := pprof.Label(ctx, "headgate.role"); got != role { + s.t.Errorf("role label = %q, want %q", got, role) + } +} + +func (s *profileLoopStore) Admit(ctx context.Context, _ AdmitRequest) ([]AdmissionUnit, error) { + s.check(ctx, "worker") + s.seen <- "admit" + return nil, nil +} + +func (s *profileLoopStore) ClaimDuty(ctx context.Context, duty, _ string, _ time.Duration) (bool, error) { + s.check(ctx, "duty") + if got, _ := pprof.Label(ctx, "headgate.duty"); got != duty { + s.t.Errorf("duty label = %q, want %q", got, duty) + } + s.seen <- duty + return false, nil +} + +func TestWorkerAndDutyProfileLabels(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + store := &profileLoopStore{t: t, seen: make(chan string, 100)} + runner := NewRunner(store, NewRegistry(), Config{DutyInterval: time.Second}) + store.worker = runner.workerID + done := make(chan error, 1) + go func() { done <- runner.Run(context.Background()) }() + synctest.Wait() + synctest.Sleep(time.Second) + runner.Shutdown() + if err := <-done; err != nil { + t.Fatal(err) + } + close(store.seen) + seen := map[string]bool{} + for role := range store.seen { + seen[role] = true + } + if !seen["admit"] { + t.Fatal("worker never admitted") + } + for _, duty := range singletonDuties { + if !seen[duty] { + t.Errorf("duty %s did not run", duty) + } + } + }) +} + func (c *captureTelemetry) rejections() [][3]any { c.mu.Lock() defer c.mu.Unlock() diff --git a/go/task_data.go b/go/task_data.go index 0485a70..5eabaf3 100644 --- a/go/task_data.go +++ b/go/task_data.go @@ -2,9 +2,9 @@ package headgate // Type-safe, in-process data for workers and job attempts. // -// Go does not permit generic methods, so the container is Extensions and the typed -// operations are package functions. The map is keyed by reflect.Type rather than a -// string. A typed box preserves even a typed nil value without an unsafe cast. +// The map is keyed by reflect.Type rather than a string. A typed box preserves +// even a typed nil value without an unsafe cast. Generic methods and the original +// package functions share the same storage and synchronization. import ( "context" @@ -28,6 +28,24 @@ type Extensions struct { func NewExtensions() *Extensions { return &Extensions{} } +// Set stores value under exactly T and returns its previous value, if present. +// Like SetExtension, it panics when extensions is nil. +func (extensions *Extensions) Set[T any](value T) (previous T, replaced bool) { + return SetExtension[T](extensions, value) +} + +// Get returns the value stored under exactly T. A nil receiver or missing type +// returns the zero value and false. +func (extensions *Extensions) Get[T any]() (value T, ok bool) { + return Extension[T](extensions) +} + +// Remove deletes and returns the value stored under exactly T. A nil receiver or +// missing type returns the zero value and false. +func (extensions *Extensions) Remove[T any]() (value T, ok bool) { + return RemoveExtension[T](extensions) +} + type extensionBox[T any] struct{ value T } func extensionType[T any]() reflect.Type { return reflect.TypeOf((*T)(nil)).Elem() } diff --git a/go/task_data_test.go b/go/task_data_test.go index 772b46e..db8163a 100644 --- a/go/task_data_test.go +++ b/go/task_data_test.go @@ -10,6 +10,67 @@ type firstExtension struct{ Value string } type secondExtension struct{ Value int } type missingExtension struct{} +func TestExtensionMethodsShareTypedState(t *testing.T) { + var extensions Extensions + if _, replaced := extensions.Set(firstExtension{"one"}); replaced { + t.Fatal("first insert unexpectedly replaced a value") + } + if got, ok := Extension[firstExtension](&extensions); !ok || got.Value != "one" { + t.Fatalf("package lookup after method insert = %#v, %v", got, ok) + } + SetExtension(&extensions, secondExtension{2}) + if got, ok := extensions.Get[secondExtension](); !ok || got.Value != 2 { + t.Fatalf("method lookup after package insert = %#v, %v", got, ok) + } + if old, replaced := extensions.Set(firstExtension{"two"}); !replaced || old.Value != "one" { + t.Fatalf("replacement = %#v, %v", old, replaced) + } + if _, ok := extensions.Get[missingExtension](); ok { + t.Fatal("wrong type matched") + } + if got, ok := extensions.Remove[firstExtension](); !ok || got.Value != "two" { + t.Fatalf("remove = %#v, %v", got, ok) + } + if _, ok := Extension[firstExtension](&extensions); ok { + t.Fatal("removed value remains visible to package lookup") + } + + // Explicit interface keys must survive forwarding without being inferred as + // the dynamic concrete type; a stored nil must remain distinct from a miss. + var pointer *firstExtension + extensions.Set[any](pointer) + if got, ok := extensions.Get[any](); !ok || got != pointer { + t.Fatalf("typed nil under interface key = %#v, %v", got, ok) + } + if _, ok := extensions.Get[*firstExtension](); ok { + t.Fatal("interface-keyed value leaked into concrete pointer key") + } + extensions.Set[*firstExtension](nil) + if got, ok := extensions.Get[*firstExtension](); !ok || got != nil { + t.Fatalf("nil pointer = %#v, %v", got, ok) + } + RemoveExtension[*firstExtension](&extensions) + if _, ok := extensions.Remove[*firstExtension](); ok { + t.Fatal("package removal was not visible to method removal") + } +} + +func TestExtensionMethodsNilReceiver(t *testing.T) { + var extensions *Extensions + if got, ok := extensions.Get[int](); ok || got != 0 { + t.Fatalf("nil receiver lookup = %d, %v", got, ok) + } + if got, ok := extensions.Remove[int](); ok || got != 0 { + t.Fatalf("nil receiver removal = %d, %v", got, ok) + } + defer func() { + if got := recover(); got != "headgate: SetExtension called with nil Extensions" { + t.Fatalf("nil receiver insert panic = %v", got) + } + }() + extensions.Set(1) +} + func TestExtensionsAreKeyedAndRetrievedByConcreteType(t *testing.T) { extensions := NewExtensions() if _, replaced := SetExtension(extensions, firstExtension{"one"}); replaced { diff --git a/scripts/bench-go-runtime.sh b/scripts/bench-go-runtime.sh new file mode 100644 index 0000000..b8fe548 --- /dev/null +++ b/scripts/bench-go-runtime.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Isolate JSON and allocator changes on the same Go 1.27 toolchain. +set -euo pipefail +cd "$(dirname "$0")/../go" +out=${1:-$(mktemp -d "${TMPDIR:-/tmp}/headgate-runtime-bench.XXXXXX")} +mkdir -p "$out" +out=$(cd "$out" && pwd) + +GOEXPERIMENT=nojsonv2 go test -c -o "$out/legacy-json" . +GOEXPERIMENT=nosizespecializedmalloc go test -c -o "$out/previous-allocator" . +GOEXPERIMENT= go test -c -o "$out/default" . +for variant in legacy-json previous-allocator default; do + : > "$out/$variant.txt" +done +# Interleave samples to reduce drift; never measure competing processes together. +for sample in {1..10}; do + for variant in legacy-json previous-allocator default; do + "$out/$variant" -test.run='^$' \ + -test.bench='Benchmark(DecodeArgs1K|TypedDispatch1K|MarshalArgs1K)$' \ + -test.benchmem -test.benchtime=200ms -test.cpu=1 >> "$out/$variant.txt" + done + echo "completed sample $sample/10" +done +if command -v benchstat >/dev/null 2>&1; then + for baseline in legacy-json previous-allocator; do + benchstat "$out/$baseline.txt" "$out/default.txt" > "$out/$baseline-vs-default.txt" + cat "$out/$baseline-vs-default.txt" + done +fi +echo "Benchmark artifacts: $out" diff --git a/scripts/verify.sh b/scripts/verify.sh index 4e3c52d..73542c2 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -38,9 +38,9 @@ echo "== ui ==" && pnpm --dir ui check echo "== rust ==" && RUST_TEST_THREADS=1 cargo test --workspace -q -- --nocapture 2>&1 | tee "$RUSTLOG" # Go only reports skip lines in verbose mode. Keep the complete transcript while showing # a compact summary in the terminal. -echo "== go ==" && (cd go && go vet ./... ./driver/headgatepgx/... ./driver/headgatemysql/... ./driver/headgateredis/... ./headgatecrypto/... ./headgateapi/... ./headgatemigrate/... ./headgatetest/... ./headgateui/... ./headgateworkflow/... \ - && go build ./... ./driver/headgatepgx/... ./driver/headgatemysql/... ./driver/headgateredis/... ./headgatecrypto/... ./headgateapi/... ./headgatemigrate/... ./headgatetest/... ./headgateui/... ./headgateworkflow/... \ - && go test -p 1 -v ./... ./driver/headgatepgx/... ./driver/headgatemysql/... ./driver/headgateredis/... ./headgatecrypto/... ./headgateapi/... ./headgatemigrate/... ./headgatetest/... ./headgateui/... ./headgateworkflow/... 2>&1 \ +echo "== go ==" && (cd go && go vet ./... ./driver/headgatepgx/... ./driver/headgatemysql/... ./driver/headgateredis/... ./headgatecrypto/... ./headgateapi/... ./headgatemigrate/... ./headgatetest/... ./headgateui/... ./headgateworkflow/... ./headgatectl/... ./headgateotel/... \ + && go build ./... ./driver/headgatepgx/... ./driver/headgatemysql/... ./driver/headgateredis/... ./headgatecrypto/... ./headgateapi/... ./headgatemigrate/... ./headgatetest/... ./headgateui/... ./headgateworkflow/... ./headgatectl/... ./headgateotel/... \ + && go test -p 1 -v ./... ./driver/headgatepgx/... ./driver/headgatemysql/... ./driver/headgateredis/... ./headgatecrypto/... ./headgateapi/... ./headgatemigrate/... ./headgatetest/... ./headgateui/... ./headgateworkflow/... ./headgatectl/... ./headgateotel/... 2>&1 \ | tee "$GOLOG" \ | awk '/^(---|===) (SKIP|FAIL)/ || /^(ok|FAIL|\?)[ \t]/ || /^ *--- SKIP/ {print}') echo "== examples ==" && ./scripts/test-examples.sh From 4ff8e39f20dbceccae67c3c9dd1e917bf1bdd277 Mon Sep 17 00:00:00 2001 From: Muhideen Mujeeb Adeoye Date: Thu, 3 Sep 2026 15:18:25 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20fix(example):=20broken=20go?= =?UTF-8?q?=20sum=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/go/go.sum | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/examples/go/go.sum b/examples/go/go.sum index e5ef611..1193e09 100644 --- a/examples/go/go.sum +++ b/examples/go/go.sum @@ -13,19 +13,13 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -<<<<<<< HEAD github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= -======= -github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= -github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= ->>>>>>> main github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -<<<<<<< HEAD github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -45,18 +39,5 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= -======= -github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= -golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= ->>>>>>> main gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=