Commit Graph

47 Commits

Author SHA1 Message Date
unkin-agent 7fd5f72de1 Bound a probe failure's weight to a window of probes
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
A backend whose health_probe_path is wrong rejects every probe, so no
success can ever arrive to clear the sticky run-failed flag. One transient
failure in the middle of those rejections excluded such a backend from the
pool for the life of the process, reintroducing the stranding bug.

Decide unhealthy vs probe_unsupported on whether a real failure landed
within the last health_probe_failures probes, floored at 3 so the window
always spans a reject/reject/failure cycle. Sustained alternation keeps a
failure in every window and stays unhealthy; an aged-out failure leaves a
pure-rejection run on probe_unsupported and back in service.
2026-09-06 00:28:01 +10:00
benvin d34782b028 Merge pull request 'Health-check backends and skip the ones that are down' (#14) from benvin/backend-health into main
Reviewed-on: #14
2026-09-06 00:18:36 +10:00
unkin-agent 8f9e4125da Count one run of failed probes, whatever kind they are
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Resetting the run on every change of outcome kind meant only consecutive
same-kind probes accumulated, so a backend failing every probe but
alternating kinds — a fronting proxy flipping 404 and 503 — never reached
the failure threshold and kept taking every query.

- count one run of consecutive not-OK probes for the down decision, and use
  the kinds only to pick which non-healthy state the run enters
- send a run containing any real failure to unhealthy; only a run of nothing
  but refusals enters probe_unsupported and stays in service
- classify 429 as a real failure: it is the backend reporting its own
  capacity, so an overloaded backend gets backed off
- document that probe_unsupported means "not verified" and that reachable is
  the /healthz field carrying actual reachability
2026-09-06 00:14:55 +10:00
unkin-agent 42fdc36737 Keep a backend whose probe path is wrong in service
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
A backend that 404s on the health probe path but serves queries fine was
marked down and excluded from every fan-out for good, since the fail-open
only triggers when no backend is left healthy.

Classify a probe reply that refuses the request itself - any 4xx, plus 501 -
as evidence about the probe, not the backend. Such a backend keeps serving
queries and reports the distinct probe_unsupported state on /healthz.
Transport failures and 5xx, 503 included, still mark a backend down.
Log the misconfiguration once per transition with the backend, probe path
and status. Track failure runs per outcome kind so a 404 run and a 503 run
never add up to one threshold.
2026-09-05 23:55:12 +10:00
unkin-agent 514377c7cb Health-check backends and skip the ones that are down
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
A down backend costs a full timeout stall on every request, since fan-out
has no way to know before it asks, and the client is never told the answer
came from fewer backends than are configured.

- poll each backend's status endpoint in the background, one goroutine per
  backend, with failure/success thresholds so a blip cannot flap it
- skip backends the prober has down, and fall open to querying all of them
  when none is left healthy
- treat a not-yet-probed backend as healthy so a restart drops no traffic
- log only up/down transitions
- stamp merged responses with X-Backends: <contributed>/<configured>
- report per-backend probe state and the last round's partiality on /healthz
- add health_probe_enabled, health_probe_path, health_probe_interval,
  health_probe_timeout, health_probe_failures and health_probe_successes,
  with matching PDBMUX_* env vars and a --health-probe flag
2026-09-05 23:30:28 +10:00
benvin 83c89ad426 Merge pull request 'feat: cache merged /facts and /nodes in memory, stale on backend failure' (#12) from benvin/cache-facts into main
Reviewed-on: #12
2026-09-05 23:15:43 +10:00
unkin-agent de61ec5081 Cache the merged body with its provenance already injected
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Rebasing onto main brought in the per-request sourceInjector, which now
runs inside the cached build: what a cache entry holds is the merged body
with pdbmux_source already stamped and upstream records of that name
already dropped.

Injecting on the way out instead would mean storing the un-injected
records plus a per-certname backend map and re-marshalling every record on
every hit, which is the work the cache exists to avoid. Baking it in stays
correct because the value names the backend that supplied the data — a
property of that fetch, not of the caller reading it — so it ages out with
the body it labels, and because the injection gate is a pure function of
path and query, both of which are already in the cache key.

Update the two cache tests whose byte-exact bodies predate the fact, and
add tests for the composition: attribution survives a cache hit on /facts
and /nodes, it ages with its entry rather than tracking a node that moved,
gated and ungated queries cache separately, and suppression of an upstream
fact of that name survives into the entry.
2026-09-05 23:05:54 +10:00
unkin-agent 45ac52df65 Store a completed build on a context detached from the flight 2026-09-05 23:01:41 +10:00
unkin-agent fc811d4eca Cancel a shared flight when its last participant leaves
Reference-count flightCall so the fan-out context ends with the last
caller waiting on it, keeping cfg.Timeout as the upper bound. A leader
leaving with a follower still parked no longer disturbs the flight, and
a solo requester disconnecting releases the upstream sockets at once
instead of holding them for the whole timeout.

Return errFlightAbandoned from Do rather than inferring the abandon path
from the request context's sentinel, and read Age off the server's clock
so it matches the timestamp the cache stored.
2026-09-05 23:01:41 +10:00
unkin-agent c7910156e8 Mark cached responses with X-Cache and Age
A stale fallback is byte-identical to a fresh response, so a client has
no way to tell it is holding data pdbmux served only because every
backend was down; the sole signal is a log line and a /healthz counter.

Set X-Cache to hit, miss or stale and Age to whole seconds since the
served copy was stored on every response from a cached path. Neither
header is emitted by OpenVoxDB, so nothing upstream is shadowed.
2026-09-05 23:01:41 +10:00
unkin-agent 743cd9a6ab Detach the shared fan-out from its leader's request context
A single flight is built by whichever request arrived first, but every
request on that key waits for it. Running the fan-out on the leader's
cancelable request context hands the leader's disconnect to followers
whose own connections are healthy: they get 502 all backends failed.
Waiters also parked on a WaitGroup, so a follower whose own client went
away stayed blocked until the leader finished.

Run the flight on a context detached from the leader's request and
bounded by the configured timeout, and pass that context into build so
the fan-out uses it. Give Do a context so a waiter can abandon a flight
it no longer needs; the leader ignores it and always runs fn to
completion, keeping the cache warm for the others. A caller that
abandons on its own cancellation writes no response.
2026-09-05 23:01:41 +10:00
unkin-agent 0c1fe7f1dd Convert a single-flight panic into an error
- flightGroup.Do recovers a panicking fn so the leader and every waiter get a non-nil error instead of a zero-value success served as 200 []
- Note in the README that facts_cache_bytes budgets body bytes only
2026-09-05 23:01:41 +10:00
unkin-agent cab1d7ade0 feat: cache merged /facts and /nodes in memory, stale on backend failure
A busy Puppetboard re-fans-out the same /facts query every few seconds, and a
502 is worse than 30-second-old facts when every PuppetDB is unreachable.

- Add a `Cache` interface (get reports fresh/stale/miss, put, stats) keyed on
  `<path>?<params>` with keys and repeated values sorted, plus a no-op default
  so uncached paths behave exactly as before.
- Route serveMerged/serveUnion/serveSummed through `serveCached`, so the
  reports cache drops in at `cacheFor` without touching a handler.
- Back /facts and /nodes with a byte-bounded LRU: `facts_ttl` (default 30s,
  clamped to a 30s cap) and `facts_cache_bytes` (default 64 MiB); expired
  entries are kept and served only when every backend fails.
- Single-flight identical keys so N concurrent requests cause one fan-out.
- Surface `cache` state and `serving_stale` in /healthz and the cache settings
  in `config show`.
2026-09-05 23:01:41 +10:00
benvin 083fb6ba53 Merge pull request 'Inject a pdbmux_source provenance fact' (#13) from benvin/source-fact into main
Reviewed-on: #13
2026-09-05 22:49:33 +10:00
unkin-agent c228597fb9 Scope the extract gate to subqueries and correct the suppression doc
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Suppression matches a record's own `name` field, so a projection that
filters on `name` without returning it carries an upstream value through.
The README claimed the record was always dropped on every query shape.
State the rule the code implements and pin the shape with a test.

`hasExtract` exempted only `in`. openvoxdb's `valid-operator?`
(src/puppetlabs/puppetdb/query_eng/engine.clj:2779-2784) lists `subquery`
separately, and the AST-rewrite stage (:2111-2123) expands
["subquery" entity expr] into ["in" cols ["extract" cols ["select_x" expr]]]
before any plan node is built, so its operand is projected into a subquery
exactly like `in`'s (:2705-2712). Exempt `subquery` and the explicit
`select_<entity>` forms (:1889-1911).

Signed-off-by: unkin-agent <unkin-agent@unkin.net>
2026-09-05 21:49:14 +10:00
unkin-agent a4a29866e1 Override the source fact on every query shape
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
- drop upstream facts of the configured name whenever the feature is enabled,
  independent of the per-query injection gate, and log the drop once per request
- walk the whole AST for a nested extract and skip injection when one is found
  outside an in subquery
- skip the environment scan on /facts when nothing is injected
- document the override rule and that PQL-syntax queries never get the fact
2026-09-05 21:31:50 +10:00
unkin-agent 6cfded36fe Merge main into benvin/source-fact
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Route /nodes through serveNodes so aggregate queries still sum, and pass the
per-request source injector into the merged path it keeps.
2026-09-05 21:05:22 +10:00
unkin-agent c935b20a54 Inject a pdbmux_source provenance fact
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
## Why
With several PuppetDBs behind one endpoint, consumers cannot tell which backend a node's data came from.

## How
- Add a synthetic `pdbmux_source` fact per certname on `/facts`, valued with the backend that won the facts merge, and stamp the same key on merged `/nodes` records.
- Emit all four fact keys including `environment`, which clients index directly.
- Skip injection for top-level `extract` queries, so `count()` and other aggregates keep the backends' own numbers, and for `/facts` queries constraining `name`; a `name` filter inside an `in` subquery still injects.
- Replace, never duplicate, an upstream fact of the configured name.
- Configure via `source_fact` / `source_fact_enabled` (`PDBMUX_SOURCE_FACT`, `PDBMUX_SOURCE_FACT_ENABLED`), defaulting to `pdbmux_source` enabled.
2026-09-05 21:00:02 +10:00
benvin 394f7df3a7 Merge pull request 'Serve PuppetDB meta and metrics endpoints, sum node and resource counts' (#11) from benvin/meta-metrics-and-counts into main
Reviewed-on: #11
2026-09-05 20:56:06 +10:00
unkin-agent b6d59af7ef Serve PuppetDB meta and metrics endpoints, sum node and resource counts
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
## Why
Puppetboard 7.0.1 cannot run against pdbmux: it exits at import when
/pdb/meta/v1/version 404s, and its landing page, metrics and radiator views
404 on the Jolokia surface.

## How
- Serve /pdb/meta/v1/version, reporting the lowest version any backend runs,
  and /pdb/meta/v1/server-time from the first reachable backend.
- Merge the Jolokia surface (/metrics/v2/read, /metrics/v2/list,
  /metrics/v1/mbeans): objects union, numeric attributes sum by default, and
  Min/Max/Uptime/StartTime plus the distribution stats take a bound or a mean.
- Route /nodes extract-count queries to the summing path ahead of the certname
  merge, and give /resources aggregates the same path.
- Document the endpoints and merge semantics in the README.
- Cover version disagreement, metric rules, escaped MBean names, count summing
  and the non-aggregate /nodes merge with httptest backends.
2026-09-05 20:41:35 +10:00
benvin 1ee7a2c07c Merge pull request 'config: drop primary/prefer and treat all backends equally' (#9) from benvin/drop-primary into main
ci/woodpecker/tag/docker Pipeline was successful
Reviewed-on: #9
v0.2.0
2026-09-05 16:07:37 +10:00
unkin-agent 8ad6205200 docs: reword reports/events union without old/new framing
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
2026-09-05 13:49:02 +10:00
unkin-agent 2391f56a11 config: drop primary/prefer and treat all backends equally
- unmerged /pdb/query/v4/* paths now go to the first backend that answers, not a designated primary
2026-09-05 13:49:02 +10:00
benvin ffc2499f98 Merge pull request 'Support a config file alongside env vars in containers' (#10) from benvin/config-file-and-env into main
Reviewed-on: #10
2026-09-05 13:42:17 +10:00
benvin d724cf0a5e Merge pull request 'feat: sum aggregate rows across backends on event-counts and /reports' (#8) from benvin/aggregate-sums into main
Reviewed-on: #8
2026-09-05 13:06:44 +10:00
unkin-agent b1ecbf31ac feat: sum aggregate rows across backends on event-counts and /reports
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
- group by the row's non-aggregate fields and add the numeric columns; X-Records on a summed endpoint is the merged row count
2026-09-05 12:29:48 +10:00
unkin-agent 03174f5ea0 Support a config file alongside env vars in containers
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
- --config / PDBMUX_CONFIG select the file; /etc/pdbmux/config.yaml joins the default search order
2026-09-05 12:29:14 +10:00
benvin 4bb44fb331 Merge pull request 'docs: strip over-commenting from README and source' (#6) from benvin/comment-cleanup into main
Reviewed-on: #6
2026-09-05 12:22:52 +10:00
unkin-agent 6ce903a6eb merge main
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
2026-09-05 11:52:24 +10:00
benvin cb05c7f377 Merge pull request 'config: drop estate-specific defaults and docs' (#7) from benvin/generic-config into main
Reviewed-on: #7
2026-09-05 11:47:20 +10:00
unkin-agent ee82b72733 Merge origin/main into benvin/generic-config
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Keep the generic wording for the package doc, --help text and README intro
while taking the /reports and /events merge from #5.
2026-09-05 11:44:28 +10:00
unkin-agent 7b9082de08 docs: strip over-commenting from server.go and reports.go
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
2026-09-05 11:42:43 +10:00
unkin-agent d88db498c0 Merge origin/main into benvin/comment-cleanup 2026-09-05 11:41:38 +10:00
unkin-agent 9d5e9d0ed8 config: drop estate-specific defaults and docs
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Remove hardcoded internal PuppetDB URLs and site-specific wording so the
project is publishable as-is.

- backends have no default; require config file or PDBMUX_BACKENDS
- primary/prefer default to the first configured backend
- config init writes example.com placeholders
- Load no longer validates, so config init/version work unconfigured
- genericise README, package doc, help text and Dockerfile comment
2026-09-05 11:40:05 +10:00
benvin 14d119f8e8 Merge pull request 'feat: merge /reports and /events across both PuppetDBs' (#5) from benvin/reports-merge into main
Reviewed-on: #5
2026-09-05 11:38:31 +10:00
unkin-agent 31ad4ae457 docs: strip over-commenting from README and source
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
2026-09-05 11:38:24 +10:00
unkin-agent 01d87412ee fix: never dedupe hash-less report rows across backends
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
An extract/count()/group_by query returns synthetic rows with no report
hash, which reportKey fell back to keying by verbatim raw bytes. Two
backends emitting a byte-identical aggregate row (e.g.
{"status":"changed","count":1}) therefore collapsed into one, silently
undercounting the merged result and contradicting the documented
guarantee that no backend's rows are dropped.

Give the mergeUnion key func an ok return: false means the record has no
dedupe identity and is always kept. reportKey returns ok=false for
hash-less rows; hash-keyed report dedupe and event verbatim-identity
dedupe are unchanged.

Add TestMergeUnion_IdenticalHashlessRowsAreNotCollapsed covering the
collision case, and reword the README line to say aggregate rows pass
through even when byte-identical.
2026-09-05 11:33:34 +10:00
unkin-agent ed2e5b73d6 feat: merge /reports and /events across both PuppetDBs
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Reports are immutable history, so a node that migrated has reports in the
old PuppetDB and the new one; serve the union rather than picking a single
owning backend as /facts does.

Re-apply order_by/limit/offset over the merged set and sum X-Records, since
each backend only orders and pages its own slice.
2026-09-05 11:22:36 +10:00
benvin e2e9004784 Merge pull request 'ci: add buildkit_config CA trust for artifactapi push' (#4) from benvin/buildx-ca-config into main
Reviewed-on: #4
2026-08-15 18:47:07 +10:00
unkin-agent a7e5a143f6 ci: add buildkit_config CA trust for artifactapi push
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
2026-08-15 18:31:05 +10:00
benvin 9fc928ee67 Merge pull request 'ci: use CA-baked plugin-docker-buildx image for artifactapi push' (#3) from benvin/buildx-ca-plugin-image into main
Reviewed-on: #3
2026-08-15 18:19:14 +10:00
unkin-agent 3c7251cc1e ci: use CA-baked plugin-docker-buildx image for artifactapi push
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
The upstream woodpeckerci/plugin-docker-buildx image does not trust the
internal CA, so buildx pushes to the artifactapi local docker registry
fail on TLS verification. Switch the docker push step to the CA-baked
plugin-docker-buildx image published to docker-internal, which bundles
the internal CA and pushes cleanly.
2026-08-15 18:04:23 +10:00
benvin c5b8b3824a Merge pull request 'ci: push images to artifactapi registry instead of gitea' (#2) from benvin/push-artifactapi into main
Reviewed-on: #2
2026-07-30 20:55:07 +10:00
unkinben f7fda175e6 ci: push images to artifactapi registry instead of gitea
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Hard switch of the docker push target from the Gitea registry to the
artifactapi local docker registry (docker-internal); the Gitea VM and its
registry are being retired. Drops the droneci/DRONECI_PASSWORD creds since
artifactapi accepts unauthenticated in-cluster pushes. Also updates the README image path.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-30 00:34:59 +10:00
benvin 85c1506450 Merge pull request 'Add pdbmux: merging PuppetDB proxy daemon' (#1) from benvin/initial into main
ci/woodpecker/tag/docker Pipeline was successful
Reviewed-on: #1
v0.1.0
2026-07-24 23:34:29 +10:00
benvin dda6b8c8c8 Add pdbmux: merging PuppetDB proxy daemon
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Split out from node-lookup PR #17 into its own repo. pdbmux presents a
single merged PuppetDB v4 query surface over the old (Consul) and new
(k8s) PuppetDBs during the VM to k8s migration, and is deployed in-cluster
via argocd-apps as a container image.
2026-07-24 23:25:43 +10:00
gitadmin 9eabb06ba0 Initial commit 2026-07-24 23:23:33 +10:00