faucet-source-redis
A Redis source for the faucet-stream ecosystem. Reads records from a Redis list, stream, or a set of keys matched by a glob pattern, yielding each record as a serde_json::Value.
Built on a lazily-opened, reused MultiplexedConnection and each mode's native paging primitive (LRANGE, XRANGE, SCAN + MGET), so it streams page-by-page without buffering the whole dataset in memory. Reach for it to drain a work queue, replay a stream's history, or load a keyspace into any faucet-stream sink.
Feature highlights
- Three source modes —
List(FIFO list elements),Stream(entries with IDs + fields), andKeys(glob-matched keys with their values). - Native streaming in all three modes —
stream_pageswalks the list, stream, or keyspace in pages so the sink writes as data arrives. - Server-side cursoring —
Keysdrives theSCANcursor manually andMGETs a page at a time, so a million-key namespace never materializes in memory at once. - Consumer-group reads —
Stream+group/consumerusesXREADGROUPon thefetch_allpath, draining all currently-pending new messages (not just one batch). - JSON-aware values — list elements, stream field values, and key values are parsed as JSON when they parse, and returned as strings otherwise.
- Connection reuse — one multiplexed connection is opened on first use and cheaply cloned across every call (no per-call TCP/AUTH handshake).
- Secrets-safe — the connection URL is masked in
Debugoutput and credentials are stripped from the lineage dataset URI.
Installation
# As a library:
# Or via the umbrella crate:
# In the CLI (opt-in connector feature):
The Redis source is opt-in — it is not in the CLI's default feature set.
Quick start
# pipeline.yaml — faucet run pipeline.yaml
version: 1
pipeline:
source:
type: redis
config:
url: redis://127.0.0.1:6379
source_type:
type: List
key: jobs:pending
sink:
type: jsonl
config:
path: ./jobs.jsonl
Configuration reference
RedisSourceConfig
| Field | Type | Default | Description |
|---|---|---|---|
url |
string | — (required) | Redis connection URL, e.g. redis://127.0.0.1:6379 or redis://user:pass@host:6379/0. Masked in Debug output. |
source_type |
RedisSourceType |
— (required) | The Redis data structure to read from — List, Stream, or Keys (see below). |
max_records |
int | (unbounded) | Optional cap on the total number of records returned. Honored by both fetch_all and streaming. |
batch_size |
int | 1000 |
Records per emitted StreamPage, mapped onto each mode's native paging primitive. 0 = no batching (drain into one page). See Streaming & batching. |
source_type — RedisSourceType
A tagged enum (type discriminator). Pick exactly one variant.
List
Reads list elements via LRANGE. Each element is parsed as JSON, falling back to a JSON string.
| Field | Type | Default | Description |
|---|---|---|---|
type |
string | — | Literal List. |
key |
string | — (required) | The list key. |
Stream
Reads stream entries via XRANGE (streaming) or XREAD / XREADGROUP (the fetch_all consumer-group convenience path).
Consumer groups apply to
fetch_allonly. The streaming path (stream_pages, used byfaucet run) always usesXRANGEand re-reads the whole stream every run — consumer-group acknowledgement can't compose with bookmark-checkpoint draining. If you setgroup/consumerand the pipeline streams, the source logs a one-shot warning; use thefetch_allpath forXREADGROUPconsume-once semantics, or dropgroup/consumerto silence it.
| Field | Type | Default | Description |
|---|---|---|---|
type |
string | — | Literal Stream. |
key |
string | — (required) | The stream key. |
group |
string | (none) | Consumer group name. When set, fetch_all uses XREADGROUP. |
consumer |
string | (none) | Consumer name within the group (required when group is set). |
count |
int | 100 (for XREADGROUP) |
Max entries read per XREAD/XREADGROUP call on the fetch_all path. Ignored by streaming, which uses batch_size. |
Each entry is returned as:
Keys
Scans keys matching a glob pattern via SCAN, then MGETs each batch.
| Field | Type | Default | Description |
|---|---|---|---|
type |
string | — | Literal Keys. |
pattern |
string | — (required) | Glob pattern for SCAN MATCH, e.g. user:*. |
Each key-value pair is returned as:
Stream field values and key values are parsed as JSON when possible; otherwise returned as strings.
Examples
Drain a list into SQLite
version: 1
name: redis_to_sqlite
pipeline:
source:
type: redis
config:
url: redis://localhost:6379
source_type:
type: List
key: jobs:pending
max_records: 10000
sink:
type: sqlite
config:
database_url: sqlite:./cache.db
table_name: jobs
column_mapping:
type: auto_map
batch_size: 500
Replay a stream's history
version: 1
pipeline:
source:
type: redis
config:
url: redis://localhost:6379
source_type:
type: Stream
key: order-events
batch_size: 2000
sink:
type: jsonl
config:
path: ./order-events.jsonl
Consumer-group read
version: 1
pipeline:
source:
type: redis
config:
url: redis://localhost:6379
source_type:
type: Stream
key: events
group: analytics-group
consumer: worker-1
count: 200
sink:
type: stdout
config:
format: jsonl
Consumer-group fields (
group/consumer) apply only to theXREADGROUPpath used byfetch_all. Streaming viastream_pagesalways usesXRANGE.
Scan a keyspace by pattern
version: 1
pipeline:
source:
type: redis
config:
url: redis://localhost:6379
source_type:
type: Keys
pattern: "session:*"
max_records: 500
sink:
type: jsonl
config:
path: ./sessions.jsonl
Streaming & batching
RedisSource overrides Source::stream_pages so the pipeline writes pages to the sink as they arrive instead of buffering the full result set. Each mode maps batch_size onto its native paging primitive:
| Mode | Native primitive | Per-page command |
|---|---|---|
List |
LRANGE start stop, sliding the window |
LRANGE <key> <start> <start + batch_size - 1> |
Stream |
XRANGE start + COUNT batch_size, advancing the start ID |
XRANGE <key> <start> + COUNT <batch_size> |
Keys |
SCAN MATCH pattern COUNT batch_size cursor → MGET per page |
SCAN <cursor> MATCH <pattern> COUNT <batch_size> then MGET key1 … keyN |
batch_size = 0 is the "no batching" sentinel — every mode drains its primitive into a single page: LRANGE 0 -1 for List, XRANGE - + for Stream, and the full SCAN cursor followed by one MGET for Keys. Useful for small lookup tables or for sinks (SQL COPY, BigQuery load jobs) that prefer one large request.
The trait-level batch_size argument passed to stream_pages is ignored in favour of RedisSourceConfig::batch_size — the config is the authoritative, user-facing knob, so a pipeline-supplied hint can never silently override an explicit config value.
Every emitted page carries bookmark: None — the Redis source has no incremental-replication / resume mode today, so it is not resumable and does not support effectively-once delivery.
Config loading
use ;
use RedisSourceConfig;
let config: RedisSourceConfig = load_json?;
let config: RedisSourceConfig = load_env_file?;
# .env
REDIS_SOURCE_URL=redis://127.0.0.1:6379
REDIS_SOURCE_MAX_RECORDS=1000
Schema introspection
use Source;
let source = new?;
println!;
Library usage
use ;
use Source;
# async
RedisSource::new is synchronous and does no I/O — the connection is opened lazily on first use. It returns Err(FaucetError::Config) only on an out-of-range batch_size. To drive it through the streaming pipeline, hand the source to faucet_core::run_stream or Pipeline.
How it works
The multiplexed connection is opened once on first use (OnceCell) and cloned per call — MultiplexedConnection shares a single underlying socket, so there is no repeated TCP/AUTH handshake. List and stream pages advance a server-side cursor (a sliding LRANGE window; the immediate-successor stream ID via next_stream_id) so no entry is re-emitted across page boundaries. Keys drives the SCAN cursor manually and MGETs a page as soon as batch_size keys accumulate, rather than materializing the whole matched keyset first; keys deleted between SCAN and MGET are dropped.
List paging consistency caveat: index-based LRANGE paging is only stable if the list is not mutated mid-scan. A concurrent LPUSH/LPOP shifts every element's index, so a writer pushing/popping while the source drains can skip or duplicate elements across page boundaries. For a queue being consumed concurrently, prefer a Redis Stream (XRANGE / consumer groups) over a list.
Lineage dataset URI
redis://<host>:<port>/<db>?key=<key> (List / Keys) or ?stream=<stream> (Stream), with credentials stripped — e.g. redis://localhost:6379/0?key=jobs:pending.
Feature flags
This crate has no optional features of its own. Enable it in the CLI / umbrella crate via the source-redis feature.
Troubleshooting / FAQ
| Symptom | Likely cause & fix |
|---|---|
FaucetError::Config: invalid Redis URL |
The url is malformed. Use the redis://[user:pass@]host:port[/db] form (or rediss:// for TLS). |
FaucetError::Config: batch_size |
batch_size exceeds MAX_BATCH_SIZE (1,000,000). Lower it, or use 0 for a single page. |
FaucetError::Source: Redis connection failed |
The server is unreachable or auth failed. Check host/port, credentials in the URL, and that Redis is running. |
XREADGROUP failed: NOGROUP |
The consumer group doesn't exist. Create it with XGROUP CREATE <key> <group> $ MKSTREAM before running. |
| Consumer-group read returns nothing | XREADGROUP with > only delivers new (never-delivered) messages. Already-pending entries for this consumer are not re-read here. |
Streaming ignores group / consumer |
Intentional — stream_pages always uses XRANGE; consumer-group semantics only apply to fetch_all. |
| List read skips/duplicates rows | The list is being mutated mid-scan (LPUSH/LPOP). Use a Redis Stream for a concurrently-consumed queue. |
Fewer key records than SCAN matched |
Keys were deleted between SCAN and MGET; missing values are dropped. Expected for a live keyspace. |
| Values arrive as strings, not objects | The stored value isn't valid JSON, so it's returned verbatim as a string. Store JSON to get parsed objects. |
See also
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.