Files
james.maesandClaude Fable 5.1 e267f4f84a
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 10s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (push) Successful in 9s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 7s
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 8s
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Successful in 8s
maven / build (push) Failing after 9s
build / maven (push) Failing after 9s
docs(release): prepare the 0.1.0 release on the 0.1 train
Add CHANGELOG.md for the first published release and docs/RELEASES.md
describing the shared ci-workflows versioning (develop SNAPSHOT,
release/0.1 RC.N, tag vX.Y.Z stable), registry consumption and the
maintainer path. README now states the 0.1 train (0.1.0-RC.1 published,
0.1.0 cut by tag) instead of the coordinated snapshot; SNAPSHOT-RELEASE.md
is marked historical. The POM revision stays 0.1.0-SNAPSHOT on develop
because the workflow owns version computation.

Verified locally: ./mvnw.cmd verify, 33 tests, 0 failures/errors/skips,
JaCoCo gates met.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 08:45:34 -06:00

180 lines
10 KiB
Markdown

# QBit Redis
Reusable Redis connections and bounded shared caches for QQQ applications.
Release train `0.1`: candidate `0.1.0-RC.1` is published to the Gitea Maven
registry (`com.kingsrook.qbits:qbit-redis`, QQQ `4.1.0-RC.21`, Java 21); the
stable `0.1.0` is cut from the same train. See [CHANGELOG.md](CHANGELOG.md) and
[release operations](docs/RELEASES.md) for consumption, versioning and the
maintainer path, and [follow-ups](docs/TODO.md) for the optional scenarios that
no release declares supported. [SNAPSHOT-RELEASE.md](docs/SNAPSHOT-RELEASE.md)
is the historical coordinated-snapshot record.
## Host integration
Register the qbit before runtime services that use Redis:
```java
new RedisQBitProducer().withQBitConfig(new RedisQBitConfig()
.withConnection("presence", new RedisConnectionConfig()
.withHost("redis.internal")
.withCredentialsProvider(new QCodeReference(HostRedisCredentials.class))))
.produce(instance);
```
The host's `RedisCredentialsProvider` resolves credentials at runtime and returns
the Jedis `RedisCredentials` interface. Do not put passwords in QBit metadata,
connection names or host strings. Provider failures are converted to safe native
connection errors so the client closes sockets opened before authentication.
TLS is enabled by default, including hostname verification. The initial transport
uses the JVM trust configuration. Hosts with private trust roots or client keys can
configure `.withTlsProvider(new QCodeReference(HostRedisTls.class))`; that provider
returns an SSLContext at runtime, and hostname verification stays enabled.
`.withTls(false)` is an explicit option for local test Redis. Real integration tests
verify authenticated TLS, untrusted-certificate rejection, wrong-host rejection,
and no plaintext fallback. Mutual-TLS tests require a separate trusted client
identity, reject missing or untrusted client certificates, and verify framework
health after a successful authenticated connection. Certificates and keys are
generated only for the disposable fixture.
The TLS provider is resolved when the Redis runtime starts. Replacing its context
alone does not reload existing pools or subscribers. For certificate replacement,
stop dependent services and restart the Redis runtime through the application
lifecycle, then restart its dependents. Qualification replaces both server and
client certificates on the same Redis endpoint: stale material fails a fresh
handshake, health becomes DOWN, and runtime restart with updated material restores
PONG and health UP while closing the old pool. This is a restart procedure, not
zero-downtime certificate rotation. Live reload, trust-chain/revocation policies,
subscriber recovery across certificate changes and failover acceptance remain open;
only standalone endpoints are implemented.
QQQ's application launcher starts the registered runtime service. Startup validates
configuration and pings every connection before exposing any of the named pools.
If any pool fails, all pools opened by that startup are closed. Duplicate starts
are rejected. Connections are isolated by QInstance.
Hosts managing runtime services manually must call `start(instance)`, then `stop()`
during service shutdown and `afterApplicationStop()` after HTTP and dependent
services have stopped. Shared pools intentionally stay open until the final callback.
## Shared values
```java
RedisCache cache = RedisAccess.cache(instance, "presence",
new RedisNamespace("production", "orders-app", tenantId, "lookup-cache"), 65536);
cache.put("record:123", encodedValue, Duration.ofSeconds(30));
Optional<byte[]> current = cache.get("record:123");
Optional<byte[]> renewed = cache.getAndTouch("record:123", Duration.ofSeconds(30));
boolean stillPresent = cache.touch("record:123", Duration.ofSeconds(30));
```
Resolve tenantId from authoritative host context. Namespace segments are encoded
separately so delimiters and Redis hash tags in a logical key cannot change scope.
Values have an explicit size limit, writes require a positive TTL, and a single SET
applies value and expiry atomically. Missing/expired values return empty; outages
throw instead of masquerading as cache misses. No wildcard delete or global scan
is exposed. Read-and-renew uses atomic GETEX (Redis 6.2+); it does not fall back
to separate commands or recreate a deleted value. Callers own serialization; this API does not deserialize Java objects.
Named connections allow separate capacity and eviction policies for disposable
caches, sessions and presence. Pool defaults are 16 connections, 500 ms maximum
borrow wait, and 2 s connection/socket timeouts. These are initial defaults, not
measured capacity recommendations. `connection.metrics()` reports active, idle,
waiting and maximum connections. `redis.connections` integrates with QQQ health
checks and reports aggregate pool utilization without endpoints or secrets.
Write-dependent hosts can opt in per connection with
`withWriteHealthKeyPrefix("staging:myapp:health:")`. Reserve that prefix exclusively
for health and permit SET on it in the connection's ACL. Each health run issues
one `SET NX PX 1000` with a random key suffix. It never overwrites an existing
key, and expiry removes probe data even if the reply is lost. The prefix is a
bounded ASCII identifier ending in a colon; do not use application data prefixes.
The default is null (connectivity only), preserving read-only and Pub/Sub-only
connections. Health details report the number of successful `writeChecks`.
A failed opted-in probe reports DOWN through the same framework check/history;
no endpoint, key, credentials or provider exception is exposed.
This is point-in-time SET acceptance, not replication durability, primary fencing,
or proof that every application command/key is authorized. A writable stale
primary can pass; topology fencing must remain an infrastructure guarantee.
The host decides whether this check gates readiness. Do not enable the probe on
an existing ACL until its dedicated prefix and write permission are configured.
This option is off by default and first ships in 0.1.0.
Other qbits consume only `com.kingsrook.qbits.redis.api`: `RedisAccess`,
`RedisNamespace`, `RedisCache`, `RedisScript`, `RedisScriptExecutor`,
`RedisCredentialsProvider` and `RedisTlsProvider`. `RedisAccess` returns cache or
script operations, without exposing pool closure or reconfiguration. Pool lifecycle,
configuration and health implementation remain in the Redis qbit's root package.
Trusted domain Lua runs through `RedisAccess.scripts(instance, name).execute(script,
keys, arguments)`. Construct immutable `RedisScript` objects once from application
resources. Redis owns EVALSHA/NOSCRIPT recovery; other failures propagate without
retry because a script may already have applied writes. Results use Java strings,
longs, nested lists and null. Domain scripts own their input bounds and must put
all keys in one cluster slot. This is not an API for untrusted user scripts.
Presence lease ordering, aggregation, permission checks and browser delivery belong
to qbit-collaboration. Its HTTP provider supports Redis; the candidate's normal
cross-node event delivery is verified in a live two-JVM browser check. Exceptional
data-loss/restore recovery is deferred to QQQ #1300. The cache API alone
is not a presence lease implementation or a distributed lock.
## Redis realtime transport (candidate)
Hosts can opt into a Redis Pub/Sub implementation of QQQ's realtime bus:
```java
new RedisQBitConfig()
.withConnection("realtime", new RedisConnectionConfig().withHost("redis.internal"))
.withRealtime("realtime", "production", "orders-app");
```
Use a dedicated named pool with at least two connections: one subscription is
reserved per node, leaving command capacity for publication. TLS/authentication
use the same runtime settings as other Redis pools. The producer refuses to
replace an already configured realtime bus. Deployment/application namespaces
isolate topics; tenant audience checks remain in the core hub and collaboration.
Messages use one Redis echo path, without local duplication or node republishing.
The subscriber only parses/enqueues; a separate dispatcher delivers to the local
hub. The queue holds at most 256 hints; payloads are limited to 64 KiB. Reconnects,
invalid/oversized hints, overflow and publication failures request a local resync.
The hub sends an empty resync event to every stream, and browsers reread authorized
state. No durable Pub/Sub history is assumed. Retries use capped jittered backoff.
Transport metrics report listening, queued, dropped, publication failures and
reconnects without exposing topics or payloads. Heartbeat reception is also counted.
A dependent runtime service starts the subscriber after the pools, without waiting
for the first browser request. Health checks observe the existing transport and
report DOWN if its subscriptions are not acknowledged, even if command PING works.
They never create a bus. Shutdown stops the transport before releasing its pool.
The realtime pool uses a 15-second native blocking-read timeout. Each node publishes
a pulse every five seconds to its own private heartbeat channel on the same
subscription connection. This keeps an idle healthy connection alive and detects
silent reads without quadratic heartbeat fanout across nodes. Pulses never reach
browser queues. Silent-stall tests keep sockets open while Redis pauses command
processing, then verify DOWN, reconnect and restored readiness.
Real Redis tests cover independent pools, application isolation, preserved audience,
forced subscriber disconnect/recovery, slow-consumer overflow and outage shutdown.
Still required before release: broader network-partition and topology failover
acceptance, lifecycle stress, realistic load and multi-node browser acceptance.
A Redis command PING alone does not establish subscriber readiness.
## Development commands
Java 21, Maven wrapper and Docker are required for the full current tests:
```powershell
.\mvnw.cmd test
```
The 0.1 release train uses the published QQQ `4.1.0-RC.21` and Jedis `7.2.0`. Tests use
real disposable Redis containers. They cover sharing, expiry, isolation, outages,
registration, failed startup cleanup, credential-error redaction, lifecycle,
authentication, bounded pool exhaustion and TLS. TLS tests generate temporary
certificates using the JDK keytool and never use credentials from external services.
See [DESIGN.md](docs/DESIGN.md) for required TLS, topology, load and release gates.