QRun-IO/ci-policy: ci / fork-guard (push) 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] (push) Successful in 9s
QRun-IO/ci-policy: native / native-result (push) Successful in 8s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) 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 / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) 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 / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) 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 / source-config-13 (push) 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 / 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 7s
QRun-IO/ci-policy: ci / fork-guard (pull_request) 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: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 10s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 8s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
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 / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) 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 / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 6s
maven / build (push) Failing after 7s
build / maven (push) Failing after 7s
Replace the in-flight snapshot coordinates that the shared-session work was built against (qqq-bom-pom 4.1.0-collab-33464d37e-SNAPSHOT, qbit-redis 0.1.0-redis-7fe5492-SNAPSHOT) with published coordinates exposed as overridable properties: qqq.version=4.1.0-RC.22, the first release-line core carrying QSessionStoreRegistry.getInstance(QInstance) and clearIfSame, and qbit-redis.version=0.1.0-RC.1, whose public api is a strict superset of the 7fe5492 snapshot. The revision stays 0.1.0-SNAPSHOT: the shared ci-workflows Maven workflow assigns 0.1.0-RC.N from release/0.1 and 0.1.0 from the v0.1.0 tag, so no version is hand-edited here. CHANGELOG records the 0.1.0 contents; README gains a compatibility matrix and the release/publication procedure for this repository. Verification (Maven 3.9.11, Temurin 21, Docker 29.9.2 for Testcontainers): mvn verify with the same sources installed locally under local-labeled versions (core release/4.1 tip f1cdf31e9 as 4.1.0-local.f1cdf31e9, qbit-redis tag v0.1.0-RC.1 as 0.1.0-local.6b0814d, because the Gitea registry needs an authenticated read from this host): 67 unit tests, 29 integration tests (RedisSessionStoreProviderIT, SharedRedisSessionStoreProviderIT, TableBasedSessionStoreProviderIT) with zero failures/errors/skips, zero Checkstyle violations, JaCoCo coverage gates met. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: James Maes <james@kof22.com>
272 lines
11 KiB
Markdown
272 lines
11 KiB
Markdown
# QBit Session Store
|
|
|
|
Pluggable session storage/caching for QQQ applications. Avoids re-deriving expensive session data (security keys, permissions) on every request.
|
|
|
|
## Why This Exists
|
|
|
|
Every QQQ request authenticates and builds a `QSession` containing security keys, permissions, and user metadata. For backends like Auth0, that means a network round-trip on every request. This QBit lets you store and reuse sessions across requests with configurable TTL, sliding expiration, and multiple storage backends.
|
|
|
|
## Project Status
|
|
|
|
**0.1.0** is the first published release line (release candidates publish as
|
|
`0.1.0-RC.N`). API may change before 1.0. See [CHANGELOG.md](CHANGELOG.md).
|
|
|
|
## Compatibility
|
|
|
|
| qbit-session-store | QQQ core (`qqq-bom-pom`) | qbit-redis (optional, `SHARED_REDIS` only) |
|
|
|--------------------|--------------------------|--------------------------------------------|
|
|
| 0.1.0 line | 4.1.0-RC.22 or later 4.1 | 0.1.0-RC.1 or later 0.1 |
|
|
|
|
The `SHARED_REDIS` provider and application-scoped registration call
|
|
`QSessionStoreRegistry.getInstance(QInstance)` and `clearIfSame`, which core
|
|
added after 4.1.0-RC.21; this QBit does not compile against RC.21 or earlier.
|
|
Artifacts publish to the Gitea Maven registry
|
|
(`https://git.kof22.com/api/packages/QRun-IO/maven`), which requires an
|
|
authenticated read.
|
|
|
|
## Features
|
|
|
|
- Four storage backends: in-memory (LRU), QQQ table, Redis, or custom
|
|
- Sliding expiration resets TTL on access
|
|
- Scheduled cleanup process for expired sessions
|
|
- Declarative QBit configuration via `QSessionStoreQBitProducer`
|
|
- Thread-safe across all providers
|
|
|
|
## Providers
|
|
|
|
| Provider | Storage | Best For |
|
|
|----------|---------|----------|
|
|
| IN_MEMORY | ConcurrentHashMap + LRU | Dev, testing, single-instance |
|
|
| TABLE_BASED | QQQ table (StoredSession) | Multi-instance, persistence |
|
|
| REDIS | Jedis with native TTL | Distributed HA deployments |
|
|
| CUSTOM | User-provided implementation | Specialized backends |
|
|
|
|
## Quick Start
|
|
|
|
Add the dependency to your project:
|
|
|
|
```xml
|
|
<dependency>
|
|
<groupId>com.kingsrook.qbits</groupId>
|
|
<artifactId>qbit-session-store</artifactId>
|
|
<version>0.1.0</version>
|
|
</dependency>
|
|
```
|
|
|
|
Produce the QBit into your QInstance:
|
|
|
|
```java
|
|
new QSessionStoreQBitProducer()
|
|
.withConfig(new QSessionStoreQBitConfig()
|
|
.withProviderType(QSessionStoreProviderType.TABLE_BASED)
|
|
.withBackendName("primaryBackend")
|
|
.withDefaultTtl(Duration.ofHours(8))
|
|
.withEnableSlidingExpiration(true))
|
|
.produce(qInstance);
|
|
```
|
|
|
|
## Configuration Options
|
|
|
|
| Field | Default | Description |
|
|
|-------|---------|-------------|
|
|
| `providerType` | (required) | IN_MEMORY, TABLE_BASED, REDIS, or CUSTOM |
|
|
| `defaultTtl` | 1 hour | Session expiration duration |
|
|
| `enableSlidingExpiration` | true | Reset TTL on access |
|
|
| `backendName` | - | Required for TABLE_BASED |
|
|
| `tableName` | "storedSession" | Table name for TABLE_BASED |
|
|
| `tableNamePrefix` | - | Prefix for table name (e.g., "app_") |
|
|
| `maxCacheSize` | 10000 | LRU size for IN_MEMORY |
|
|
| `redisHost` | - | Required for REDIS |
|
|
| `redisPort` | 6379 | Redis port |
|
|
| `redisPassword` | - | Redis password (optional) |
|
|
| `redisKeyPrefix` | "qqq:session:" | Redis key namespace |
|
|
| `enableCleanupProcess` | true | Enable scheduled cleanup |
|
|
| `cleanupIntervalSeconds` | 300 | Cleanup interval (5 min) |
|
|
|
|
## Table Schema (TABLE_BASED Provider)
|
|
|
|
The QBit automatically creates the table metadata. You must create the physical table in your database:
|
|
|
|
```sql
|
|
-- PostgreSQL
|
|
CREATE TABLE stored_session (
|
|
id SERIAL PRIMARY KEY,
|
|
session_uuid VARCHAR(36) NOT NULL UNIQUE,
|
|
user_id VARCHAR(255),
|
|
session_data TEXT,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
create_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
modify_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- With table prefix (e.g., tableNamePrefix="app_")
|
|
CREATE TABLE app_stored_session (
|
|
-- same columns as above
|
|
);
|
|
```
|
|
|
|
**Column mapping:**
|
|
- `id` - Auto-increment primary key
|
|
- `session_uuid` - UUID identifying the session (unique index)
|
|
- `user_id` - User ID reference for auditing
|
|
- `session_data` - Serialized QSession as JSON
|
|
- `expires_at` - Session expiration timestamp
|
|
- `create_date` / `modify_date` - Audit timestamps
|
|
|
|
## Usage Examples
|
|
|
|
### In-Memory (Development)
|
|
|
|
```java
|
|
new QSessionStoreQBitProducer()
|
|
.withConfig(new QSessionStoreQBitConfig()
|
|
.withProviderType(QSessionStoreProviderType.IN_MEMORY)
|
|
.withMaxCacheSize(1000)
|
|
.withDefaultTtl(Duration.ofMinutes(30)))
|
|
.produce(qInstance);
|
|
```
|
|
|
|
### Table-Based with Prefix
|
|
|
|
```java
|
|
new QSessionStoreQBitProducer()
|
|
.withConfig(new QSessionStoreQBitConfig()
|
|
.withProviderType(QSessionStoreProviderType.TABLE_BASED)
|
|
.withBackendName("primaryBackend")
|
|
.withTableNamePrefix("myapp_") // Results in "myapp_storedSession" table
|
|
.withDefaultTtl(Duration.ofHours(8)))
|
|
.produce(qInstance);
|
|
```
|
|
|
|
### Redis
|
|
|
|
```java
|
|
new QSessionStoreQBitProducer()
|
|
.withConfig(new QSessionStoreQBitConfig()
|
|
.withProviderType(QSessionStoreProviderType.REDIS)
|
|
.withRedisHost("redis.example.com")
|
|
.withRedisPort(6379)
|
|
.withRedisPassword("secret")
|
|
.withRedisKeyPrefix("myapp:session:")
|
|
.withDefaultTtl(Duration.ofHours(4)))
|
|
.produce(qInstance);
|
|
```
|
|
|
|
### Shared Redis infrastructure
|
|
|
|
Use `SHARED_REDIS` when the application installs `qbit-redis`. Include that optional
|
|
artifact explicitly in the host and produce its named connection metadata before
|
|
session-store. The Redis runtime owns TLS, credentials, bounded pools and health.
|
|
Session-store owns JSON serialization, session expiry and payload limits.
|
|
|
|
```java
|
|
new QSessionStoreQBitProducer()
|
|
.withConfig(new QSessionStoreQBitConfig()
|
|
.withProviderType(QSessionStoreProviderType.SHARED_REDIS)
|
|
.withSharedRedis(new SharedRedisSessionStoreSettings(
|
|
"sessions", "production", "orders-app", "authentication", 65536))
|
|
.withDefaultTtl(Duration.ofHours(1))
|
|
.withEnableSlidingExpiration(true))
|
|
.produce(qInstance);
|
|
```
|
|
|
|
Settings are connection name, deployment, application, authentication-domain scope,
|
|
and maximum serialized bytes. Nodes serving the same application/domain must use
|
|
identical scope; distinct applications/domains must use distinct scope. Resolve it
|
|
from host configuration, never an unauthenticated request. Redis namespacing encodes
|
|
each segment independently. This provider does not interpret per-user tenant claims.
|
|
|
|
Operations require the owning Redis runtime to be started. The provider never opens
|
|
or closes a pool and never falls back to another connection. `loadAndTouch` uses
|
|
atomic GETEX when sliding is enabled, plain GET otherwise; Redis 6.2+ is required.
|
|
Missing values are cache misses; infrastructure, size and decoding failures throw.
|
|
Failed remove also throws so logout can retry. TTLs must be 1 ms through 365 days;
|
|
serialized payload bounds may be configured up to 1 MiB. No cleanup job is produced
|
|
because Redis owns expiry. Framework Redis and session-store health checks report
|
|
availability; the provider's status string is only descriptive.
|
|
|
|
The existing `REDIS` provider retains its independent legacy pool/configuration.
|
|
Selecting `SHARED_REDIS` is explicit and uses a new namespace; it does not migrate
|
|
existing cache entries. A framework runtime service releases its owned registry/context entries in
|
|
`afterApplicationStop`, after dependent services have stopped; it never closes
|
|
the Redis pool. Repeated cleanup is harmless and preserves replacement providers.
|
|
Restart reconstructs registration from metadata after Redis starts. Manual hosts
|
|
must honor the same runtime callbacks. Shared providers weakly reference their
|
|
application so an abandoned pre-start registration cannot retain the owner.
|
|
|
|
Local acceptance covers real HTTP authentication/logout and Redis outage/retry
|
|
through the core sample's `redis-acceptance` profile. Coordinated artifact
|
|
publication and clean host adoption remain release gates. Redis failover topology
|
|
qualification is future work tracked in [QQQ #1301](https://github.com/QRun-IO/qqq/issues/1301).
|
|
|
|
### Custom Provider
|
|
|
|
```java
|
|
new QSessionStoreQBitProducer()
|
|
.withConfig(new QSessionStoreQBitConfig()
|
|
.withProviderType(QSessionStoreProviderType.CUSTOM)
|
|
.withCustomProviderCodeReference(new QCodeReference(MyCustomProvider.class)))
|
|
.produce(qInstance);
|
|
```
|
|
|
|
## Accessing the Provider
|
|
|
|
After producing the QBit, retrieve the provider for its owning application.
|
|
Within a request, `getProvider()` uses `QContext.getQInstance()`. Startup and
|
|
background callers should use the explicit overload below. The qbit automatically
|
|
registers against the QInstance supplied to `produce`; it never installs a
|
|
JVM-wide fallback for other applications.
|
|
|
|
Hosts registering directly with core must use
|
|
`QSessionStoreRegistry.getInstance(qInstance).register(provider)` outside request
|
|
context. See [registration migration](docs/SESSION-REGISTRATION.md).
|
|
|
|
```java
|
|
QSessionStoreProviderInterface provider = QSessionStoreQBitContext.getProvider(qInstance);
|
|
|
|
// Store a session
|
|
provider.store(session.getUuid(), session, Duration.ofHours(1));
|
|
|
|
// Load a session
|
|
Optional<QSession> cached = provider.load(sessionUuid);
|
|
|
|
// Touch (refresh TTL)
|
|
provider.touch(sessionUuid);
|
|
|
|
// Remove
|
|
provider.remove(sessionUuid);
|
|
```
|
|
|
|
## Contributing
|
|
|
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
|
|
## Releasing
|
|
|
|
Versions are computed by the shared `KofTwentyTwo/ci-workflows` Maven workflow
|
|
(`.gitea/workflows/build.yaml`) from what is pushed; the `revision` property on
|
|
`develop` stays `0.1.0-SNAPSHOT` and is never hand-edited for a release.
|
|
|
|
| Push | Published version | Side effects |
|
|
|----------------------------|--------------------------|---------------------------------------------------------------|
|
|
| `develop` | `0.1.0-SNAPSHOT` | none |
|
|
| branch `release/0.1` | `0.1.0-RC.1`, `RC.2`, .. | commits "Bump version to X [skip ci]" to the branch, tags `vX` |
|
|
| tag `v0.1.0` | `0.1.0` | pushes the version bump to `main` |
|
|
| tag `publish-<name>` | `0.1.0-<feature>-<sha>-SNAPSHOT` | none |
|
|
|
|
To cut the first release: merge this release preparation into `develop`, create
|
|
`release/0.1` from `develop` and push it (publishes `0.1.0-RC.1`), qualify the
|
|
candidate, then tag the bumped `release/0.1` head `v0.1.0` and push the tag
|
|
(publishes `0.1.0`). Merge `release/0.1` back into `develop`; the workflow then
|
|
advances `develop` to `0.2.0-SNAPSHOT`. Record the date in `CHANGELOG.md` when
|
|
tagging. GitHub is a read-only shadow; never push there.
|
|
|
|
## License
|
|
|
|
Apache-2.0 - See [LICENSE](LICENSE)
|
|
|
|
Launcher acceptance covers normal shutdown, failure of a later runtime, and Redis
|
|
authentication failure before session-runtime startup followed by retry. In that
|
|
early failure case no Redis pool is published; the weak pre-start registration
|
|
remains configured but cannot execute session operations until Redis starts.
|