greentic-session 0.1.0

Greentic multi-tenant session manager with in-memory and Redis backends
Documentation

greentic-session

Greentic’s session manager provides a multi-tenant coordination layer for connector flows.
It offers deterministic session key mapping, optimistic concurrency via compare-and-set tokens, outbox deduplication, TTL management, and pluggable persistence backends.

Crate Highlights

  • Shared model – Reuses greentic-types primitives (GResult, TenantCtx, etc.) while defining session-centric structs (Session, SessionMeta, SessionCursor, OutboxEntry, Cas).
  • Backends – In-memory (dashmap + lazy TTL cleanup) is always available; Redis backend is feature-gated (redis, default-enabled) and stores data under greentic:session:{tenant}:{key}.
  • Deterministic keys – Helper mappers hash connector payload hints (e.g., Telegram updates, webhooks) to produce stable SessionKey values without leaking PII.
  • Concurrency guarantee – CAS tokens (Cas) increment on every successful write, giving last-writer-wins semantics and protecting against racey updates.
  • Outbox dedupe – Repeated (seq, payload_sha256) pairs are ignored in both backends, ensuring idempotent connector hand-offs.

Choosing a Backend

Feature flag combo Backend availability Suggested usage
default (redis) Redis + in-memory Production deployments with Redis
--no-default-features --features inmemory In-memory only Tests, single-node dev
--all-features Redis + schema export CI / schema docs

Enable the Redis backend with the default feature set. In environments without Redis, disable default features and opt into inmemory.

Quickstart

cargo run --example quickstart
# optionally, export REDIS_URL and re-run to exercise Redis

The example walks through:

  1. Mapping a Telegram update onto a SessionKey.
  2. Creating a Session and writing it with SessionStore::put.
  3. Fetching the CAS token and issuing a CAS update with a new outbox entry.
  4. Refreshing TTL/updated_at via touch and finally deleting the session.
  5. Running the same flow against Redis (when REDIS_URL is configured).

You can adapt the snippet for your flow orchestration logic. A minimal excerpt:

use greentic_session::{inmemory::InMemorySessionStore, mapping::telegram_update_to_session_key, model::*};
use greentic_session::SessionStore;
use time::OffsetDateTime;

let store = InMemorySessionStore::new();
let key = telegram_update_to_session_key("bot", "chat", "user");
let session = Session {
    id: SessionId::new(),
    key: key.clone(),
    cursor: SessionCursor { flow_id: "onboard".into(), node_id: "start".into(), wait_reason: None, outbox_seq: 0 },
    meta: SessionMeta { tenant_id: "tenant-42".into(), team_id: None, user_id: None, labels: serde_json::Map::new() },
    outbox: vec![],
    updated_at: OffsetDateTime::now_utc(),
    ttl_secs: 60,
};
let cas = store.put(session)?;
// later…
if let Some((mut current, current_cas)) = store.get(&key)? {
    current.cursor.outbox_seq += 1;
    match store.update_cas(current, current_cas)? {
        Ok(next) => println!("updated with cas {next:?}"),
        Err(conflict) => println!("retry with current cas {conflict:?}"),
    }
}

Deterministic Session Keys

  • mapping::telegram_update_to_session_key(bot_id, chat_id, user_id)
  • mapping::webhook_to_session_key(source, subject, id_hint)

Both functions derive a SHA-256 digest and encode it as hex. Avoid placing secrets or raw PII in the inputs—hash inputs or substitute stable, non-sensitive identifiers extracted earlier in your pipeline.

TTL & Cleanup Semantics

  • Each Session carries an absolute ttl_secs. put and update_cas stamp updated_at to OffsetDateTime::now_utc() and recompute the expiry.
  • In-memory backend performs lazy expiration during read/write/touch operations and periodically scans (no more often than every 60 s) to evict expired entries.
  • Redis backend stores the envelope (session + cas) in a single JSON blob and keeps a lookup key for tenant resolution. TTL changes are applied atomically via Lua, and lookup keys mirror the data TTL (or are persisted when TTL is zero).
  • touch refreshes both updated_at and TTL; pass None to keep the existing TTL but still bump updated_at.

Thread-safety & Performance

  • InMemorySessionStore uses DashMap for lock-free reads and employs a parking_lot mutex only for occasional cleanup scheduling.
  • RedisSessionStore relies on Lua scripts for CAS updates to guarantee atomicity and uses Redis-native expiration for TTL enforcement. A secondary lookup key (greentic:session:lookup:{hash}) tracks tenant-to-key mapping.
  • Benchmarks (cargo bench) provide baseline throughput for put, update_cas, and get on the in-memory backend via Criterion.

Testing & Linting

cargo fmt --all
cargo clippy --all-targets -- -D warnings
cargo test --all-features
cargo test --no-default-features --features inmemory

Redis integration tests (ttl_and_touch, cas_races, outbox_dedupe) require REDIS_URL. CI spins up Redis automatically via a service container.

Local Redis for ad-hoc runs:

docker run --rm -p 6379:6379 redis:7-alpine
export REDIS_URL=redis://localhost:6379

Versioning & Stability

  • Crate metadata follows Semantic Versioning. Initial releases start at 0.x while APIs and models stabilize.
  • Publishing is tag-driven (v* tags). GitHub Actions handle fmt/clippy/test for pull requests and main pushes.
  • Licensing: MIT (see LICENSE). When embedding this crate elsewhere, keep license headers aligned.

Maintenance Notes

  • Extend shared surface area (e.g., adding new fields to Session) through greentic-types first to avoid duplication.
  • Additional backends (SQL, DynamoDB, etc.) should live behind new feature flags and reuse the SessionStore trait.
  • Observe the TenantCtx semantics from the next-gen overview—tenant-aware routing is preserved via the lookup key strategy on Redis and via the SessionMeta struct for in-memory computations.

Releases & Publishing

  • Crate versions are sourced directly from each Cargo.toml.
  • Every push to master reruns the auto-tag workflow. When a crate’s manifest changes its version and no matching tag exists yet, a git tag <crate-name>-v<semver> is created and pushed.
  • The publish workflow lints, builds, and tests the full workspace (all features) before invoking katyo/publish-crates@v2.
  • Publishing is idempotent: if a crate version is already on crates.io, the workflow succeeds without re-uploading.
  • Configure the CARGO_REGISTRY_TOKEN secret with a crates.io publish token to enable automated releases.