streamgate 0.5.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {streamgate-0.5.0 → streamgate-2.0.0}/.github/workflows/ci.yml +11 -6
- streamgate-2.0.0/CHANGELOG.md +556 -0
- streamgate-2.0.0/CONFIGURATION.md +190 -0
- streamgate-2.0.0/PKG-INFO +249 -0
- streamgate-2.0.0/README.md +213 -0
- streamgate-2.0.0/README.zh-CN.md +409 -0
- streamgate-2.0.0/examples/http_probe/consume.py +43 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/examples/http_probe/health_server.py +4 -4
- {streamgate-0.5.0 → streamgate-2.0.0}/examples/http_probe/models.py +1 -1
- {streamgate-0.5.0 → streamgate-2.0.0}/examples/http_probe/produce.py +15 -34
- {streamgate-0.5.0 → streamgate-2.0.0}/examples/prod_pipeline/README.md +19 -18
- streamgate-2.0.0/examples/prod_pipeline/consume.py +59 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/examples/prod_pipeline/health_server.py +4 -4
- {streamgate-0.5.0 → streamgate-2.0.0}/examples/prod_pipeline/models.py +6 -6
- streamgate-2.0.0/examples/prod_pipeline/produce.py +87 -0
- streamgate-2.0.0/examples/pure_pipeline/README.md +62 -0
- streamgate-2.0.0/examples/pure_pipeline/consume.py +76 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/examples/pure_pipeline/models.py +1 -1
- streamgate-2.0.0/examples/pure_pipeline/produce.py +51 -0
- streamgate-2.0.0/examples/redis_admission/README.md +51 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/examples/redis_admission/models.py +1 -1
- streamgate-2.0.0/examples/redis_admission/produce.py +68 -0
- streamgate-2.0.0/examples/sqlite_upsert/README.md +62 -0
- streamgate-2.0.0/examples/sqlite_upsert/consume.py +33 -0
- streamgate-2.0.0/examples/sqlite_upsert/models.py +22 -0
- streamgate-2.0.0/examples/sqlite_upsert/produce.py +36 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/pyproject.toml +3 -3
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/__init__.py +50 -46
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/config.py +6 -26
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/consumer/classifier.py +6 -6
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/consumer/dlq.py +91 -94
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/consumer/loop.py +206 -233
- streamgate-2.0.0/src/streamgate/consumer/options.py +261 -0
- streamgate-2.0.0/src/streamgate/consumer/runner.py +309 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/contrib/__init__.py +3 -2
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/contrib/http_probe/probe_signal.py +1 -1
- streamgate-2.0.0/src/streamgate/contrib/mssql_upsert/__init__.py +73 -0
- streamgate-2.0.0/src/streamgate/contrib/redis_dedup/__init__.py +45 -0
- streamgate-2.0.0/src/streamgate/contrib/redis_dedup/cache.py +182 -0
- streamgate-2.0.0/src/streamgate/contrib/redis_dedup/carrier.py +380 -0
- streamgate-2.0.0/src/streamgate/contrib/redis_dedup/config.py +16 -0
- streamgate-2.0.0/src/streamgate/contrib/redis_dedup/group_cache.py +213 -0
- streamgate-2.0.0/src/streamgate/contrib/redis_dedup/group_carrier.py +472 -0
- streamgate-2.0.0/src/streamgate/contrib/sql_upsert/__init__.py +37 -0
- {streamgate-0.5.0/src/streamgate/contrib/sql_sink → streamgate-2.0.0/src/streamgate/contrib/sql_upsert}/_dialects/__init__.py +1 -1
- {streamgate-0.5.0/src/streamgate/contrib/sql_sink → streamgate-2.0.0/src/streamgate/contrib/sql_upsert}/backfill.py +60 -30
- {streamgate-0.5.0/src/streamgate/contrib/sql_sink → streamgate-2.0.0/src/streamgate/contrib/sql_upsert}/classifier.py +4 -4
- {streamgate-0.5.0/src/streamgate/contrib/sql_sink → streamgate-2.0.0/src/streamgate/contrib/sql_upsert}/config.py +2 -2
- {streamgate-0.5.0/src/streamgate/contrib/sql_sink → streamgate-2.0.0/src/streamgate/contrib/sql_upsert}/engines.py +1 -1
- streamgate-2.0.0/src/streamgate/contrib/sql_upsert/factory.py +77 -0
- {streamgate-0.5.0/src/streamgate/contrib/sql_sink → streamgate-2.0.0/src/streamgate/contrib/sql_upsert}/upsert.py +72 -38
- streamgate-2.0.0/src/streamgate/contrib/sqlite_upsert/__init__.py +72 -0
- streamgate-2.0.0/src/streamgate/ingest/dedup/in_memory.py +123 -0
- streamgate-2.0.0/src/streamgate/ingest/producer.py +842 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/obs/metrics.py +7 -7
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/protocols.py +112 -117
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/resilience/health.py +61 -60
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/transport/kafka.py +29 -20
- streamgate-0.5.0/CHANGELOG.md +0 -242
- streamgate-0.5.0/CONFIGURATION.md +0 -139
- streamgate-0.5.0/PKG-INFO +0 -202
- streamgate-0.5.0/README.md +0 -166
- streamgate-0.5.0/README.zh-CN.md +0 -315
- streamgate-0.5.0/examples/http_probe/consume.py +0 -53
- streamgate-0.5.0/examples/prod_pipeline/consume.py +0 -96
- streamgate-0.5.0/examples/prod_pipeline/produce.py +0 -103
- streamgate-0.5.0/examples/pure_pipeline/README.md +0 -61
- streamgate-0.5.0/examples/pure_pipeline/consume.py +0 -80
- streamgate-0.5.0/examples/pure_pipeline/produce.py +0 -69
- streamgate-0.5.0/examples/redis_admission/README.md +0 -50
- streamgate-0.5.0/examples/redis_admission/produce.py +0 -84
- streamgate-0.5.0/examples/sqlite_sink/README.md +0 -53
- streamgate-0.5.0/examples/sqlite_sink/consume.py +0 -90
- streamgate-0.5.0/examples/sqlite_sink/models.py +0 -22
- streamgate-0.5.0/examples/sqlite_sink/produce.py +0 -58
- streamgate-0.5.0/src/streamgate/consumer/runner.py +0 -251
- streamgate-0.5.0/src/streamgate/contrib/mssql_sink/__init__.py +0 -30
- streamgate-0.5.0/src/streamgate/contrib/redis_admission/__init__.py +0 -39
- streamgate-0.5.0/src/streamgate/contrib/redis_admission/admission.py +0 -501
- streamgate-0.5.0/src/streamgate/contrib/redis_admission/config.py +0 -18
- streamgate-0.5.0/src/streamgate/contrib/redis_admission/existence.py +0 -236
- streamgate-0.5.0/src/streamgate/contrib/sql_sink/__init__.py +0 -36
- streamgate-0.5.0/src/streamgate/contrib/sqlite_sink/__init__.py +0 -29
- streamgate-0.5.0/src/streamgate/ingest/admission/in_memory.py +0 -97
- streamgate-0.5.0/src/streamgate/ingest/admission/no_admission.py +0 -47
- streamgate-0.5.0/src/streamgate/ingest/gateway.py +0 -370
- streamgate-0.5.0/src/streamgate/ingest/producer.py +0 -299
- streamgate-0.5.0/src/streamgate/specs.py +0 -91
- {streamgate-0.5.0 → streamgate-2.0.0}/.github/workflows/release.yml +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/.gitignore +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/AGENTS.md +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/CONTRIBUTING.md +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/LICENSE +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/examples/docker-compose.yml +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/examples/http_probe/README.md +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/consumer/__init__.py +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/contrib/_deps.py +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/contrib/http_probe/__init__.py +0 -0
- {streamgate-0.5.0/src/streamgate/contrib/sql_sink → streamgate-2.0.0/src/streamgate/contrib/sql_upsert}/_dialects/mssql.py +0 -0
- {streamgate-0.5.0/src/streamgate/contrib/sql_sink → streamgate-2.0.0/src/streamgate/contrib/sql_upsert}/_dialects/sqlite.py +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/ingest/__init__.py +0 -0
- {streamgate-0.5.0/src/streamgate/ingest/admission → streamgate-2.0.0/src/streamgate/ingest/dedup}/__init__.py +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/obs/__init__.py +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/obs/logging.py +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/resilience/__init__.py +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/resilience/backpressure.py +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/transport/__init__.py +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/src/streamgate/transport/codec.py +0 -0
- {streamgate-0.5.0 → streamgate-2.0.0}/uv.lock +0 -0
|
@@ -100,15 +100,20 @@ jobs:
|
|
|
100
100
|
assert streamgate.__version__
|
|
101
101
|
|
|
102
102
|
from streamgate import (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
Consumer, ConsumerOptions, DedupOptions, DefaultErrorClassifier,
|
|
104
|
+
ErrorKind, InMemoryDedupCarrier, ManualBackpressureSignal,
|
|
105
|
+
Producer, ProducerOptions,
|
|
106
106
|
)
|
|
107
107
|
|
|
108
108
|
try:
|
|
109
|
-
|
|
109
|
+
Consumer()
|
|
110
|
+
except TypeError as e:
|
|
111
|
+
assert "handler" in str(e)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
Producer()
|
|
110
115
|
except ValueError as e:
|
|
111
|
-
assert "
|
|
116
|
+
assert "topic is required" in str(e)
|
|
112
117
|
|
|
113
118
|
print("bare-install import smoke OK")
|
|
114
119
|
EOF
|
|
@@ -120,7 +125,7 @@ jobs:
|
|
|
120
125
|
sleep 15
|
|
121
126
|
KAFKA__BOOTSTRAP_SERVERS=localhost:29092 ../../../bare/bin/python produce.py | tee produce.out
|
|
122
127
|
grep -q "o-1: accepted" produce.out
|
|
123
|
-
grep -q "o-1:
|
|
128
|
+
grep -q "o-1: duplicate" produce.out # in-process dedup contract
|
|
124
129
|
grep -q "o-2: accepted" produce.out
|
|
125
130
|
sleep 20 # allow batch-timeout flush + offset commit
|
|
126
131
|
kill -INT $CONSUMER_PID
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [2.0.0] - 2026-09-11
|
|
9
|
+
|
|
10
|
+
### BREAKING — unified backfill contract (feat!)
|
|
11
|
+
|
|
12
|
+
`BackfillSource.load` / `SqlBackfill.load` now speak one mapped contract:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
await load(scope: str) -> dict[str, JsonObject] | None
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
where `scope` is an identity key (single-key carriers) or a group id (group
|
|
19
|
+
carriers), and the return is a `{identity: summary}` mapping — `None` and `{}`
|
|
20
|
+
are equivalent (no records).
|
|
21
|
+
|
|
22
|
+
- old single-summary `None` (confirmed absent) → `{}` / key-not-present
|
|
23
|
+
- old single-summary hit → `{identity: summary}`
|
|
24
|
+
- duplicate / allow judgment paths are preserved verbatim
|
|
25
|
+
|
|
26
|
+
**Runtime breakage:** external custom `BackfillSource` implementations must
|
|
27
|
+
switch `load`'s return shape to the mapped form. Mechanical migration, no
|
|
28
|
+
other surface changed.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`RedisGroupDedupCarrier`** — a group-scoped `DedupCarrier`
|
|
33
|
+
(`guarantee="distributed"`) in `streamgate.contrib.redis_dedup`. A group
|
|
34
|
+
(batch / task / import session) is one Redis HASH
|
|
35
|
+
(`{prefix}dedup_group:{group}`, field = group-internal identity);
|
|
36
|
+
`group_key(record)` extracts the group id, `key(record)` the identity.
|
|
37
|
+
Three-stage admit: in-group HASH hit → group-exists fast path (**zero DB**) →
|
|
38
|
+
new-group cold load of the **whole group** (group-level single-flight +
|
|
39
|
+
concurrency gate). The group key's lifecycle is immutable — created only by a
|
|
40
|
+
successful whole-group backfill or the first placeholder after an
|
|
41
|
+
empty-group confirm — so a present group key makes in-group judgment
|
|
42
|
+
authoritative with no safety valve.
|
|
43
|
+
- **`RedisGroupDedupCache`** — HASH storage for the group carrier: atomic
|
|
44
|
+
per-field Lua placeholder, idempotent HSET+EXPIRE writes, chunked-pipeline
|
|
45
|
+
whole-group backfill, `group_exists` / `get_group_fields` / `get_ttl` /
|
|
46
|
+
`delete_group`.
|
|
47
|
+
- **`SqlBackfill(group_column=...)`** — optional group-mode backfill
|
|
48
|
+
(`WHERE group_column = :scope`, returns the whole group); unset keeps the
|
|
49
|
+
1.0.0 single-key behavior unchanged.
|
|
50
|
+
- `RedisConfig.group_key_prefix` / `RedisConfig.group_ttl_seconds` for the
|
|
51
|
+
group HASH keys.
|
|
52
|
+
|
|
53
|
+
### Notes
|
|
54
|
+
|
|
55
|
+
- The 1.0.0 single-key `RedisDedupCarrier` / `RedisDedupCache` keep their public
|
|
56
|
+
API; only their internal consumption of `load` moved to the mapped contract.
|
|
57
|
+
Both carriers coexist in one Redis instance (`dedup:` STRING vs
|
|
58
|
+
`dedup_group:` HASH, no type clash).
|
|
59
|
+
- Large active groups: whole-group backfill is chunked internally (implementation
|
|
60
|
+
detail — no config, no cap). Mind the `group_ttl_seconds` vs write-rate
|
|
61
|
+
relationship (idle-GC renewal grows the memory watermark of active groups);
|
|
62
|
+
a low-rate group that crosses its TTL mid-batch simply re-sources once, which
|
|
63
|
+
is normal self-healing.
|
|
64
|
+
|
|
65
|
+
## [1.0.0] - 2026-09-10
|
|
66
|
+
|
|
67
|
+
### BREAKING — both sides redesigned as flat entry points
|
|
68
|
+
|
|
69
|
+
**Consumer side — a "data outlet".** The consumer API stopped modeling
|
|
70
|
+
"persistence" and now models what it always was: a consume loop. You hand it a
|
|
71
|
+
handler; offsets, retries, self-healing, poison isolation and graceful
|
|
72
|
+
shutdown are the framework's job. Single-record vs. batch is just
|
|
73
|
+
`batch_size=1` vs `batch_size=N`.
|
|
74
|
+
|
|
75
|
+
**Producer side — a "door".** The producer API stopped modeling the
|
|
76
|
+
"entity + slot" business scenario and the two-hop `IngestBinding →
|
|
77
|
+
IngestGateway` assembly. `Producer(bootstrap_servers, topic, key, options)`
|
|
78
|
+
is the whole entry: dedup is opt-in (`DedupOptions`, one line for the
|
|
79
|
+
in-process carrier), backpressure folds into `options`, and the routing key
|
|
80
|
+
(`key=`) is separated from the identity key (`DedupOptions.key`) — they were
|
|
81
|
+
welded together in 0.x and that welding was the root cause of every awkward
|
|
82
|
+
semantic.
|
|
83
|
+
|
|
84
|
+
**Migration guide (old → new):**
|
|
85
|
+
|
|
86
|
+
Constructing the consumer:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
old: ConsumeSpec(on_record=h) + ConsumerWorker(spec, kafka_config=..., consumer_config=...)
|
|
90
|
+
new: Consumer(bootstrap_servers=..., topic=..., group_id=..., handler=h)
|
|
91
|
+
|
|
92
|
+
old: ConsumeSpec(sink=UpsertWriter(...)) + ConsumerWorker(..., error_classifier=...)
|
|
93
|
+
new (out-of-the-box): SqliteConsumer(db=..., upserts=[...], topic=..., group_id=...)
|
|
94
|
+
new (self-assembled): Consumer(..., handler=h, probe=p,
|
|
95
|
+
options=ConsumerOptions(classifier=...))
|
|
96
|
+
|
|
97
|
+
old: batch_size=1 (on ConsumerConfig)
|
|
98
|
+
new: Consumer(..., batch_size=1)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Constructing the producer:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
old: IngestBinding(...) + IngestGateway(binding=..., kafka_config=..., backpressure_config=...)
|
|
105
|
+
new: Producer(bootstrap_servers=..., topic=..., options=ProducerOptions(...))
|
|
106
|
+
|
|
107
|
+
old: gateway.process(record, overwrite=True)
|
|
108
|
+
new: producer.push(record, force=True)
|
|
109
|
+
|
|
110
|
+
old: IngestBinding(admission="in-memory", entity_key=..., slot_key=..., summary=...)
|
|
111
|
+
new: ProducerOptions(dedup=DedupOptions(key=..., summary=...)) # in-process carrier
|
|
112
|
+
|
|
113
|
+
old: IngestBinding(admission="none")
|
|
114
|
+
new: omit dedup entirely (pure push, zero dedup concepts)
|
|
115
|
+
|
|
116
|
+
old: outcome = await gateway.process(record); outcome.kind == "conflict"
|
|
117
|
+
new: result = await producer.push(record); result.kind == "duplicate"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
API renames / removals (consumer side):
|
|
121
|
+
|
|
122
|
+
| 0.x | 1.0.0 |
|
|
123
|
+
|-----|-------|
|
|
124
|
+
| `ConsumerWorker(spec, kafka_config=..., consumer_config=..., ...)` | `Consumer(bootstrap_servers, topic, group_id, handler, batch_size, flush_timeout, options)` |
|
|
125
|
+
| `ConsumeSpec` (deleted) | flat constructor arguments + `ConsumerOptions` |
|
|
126
|
+
| `RecordWriter` protocol + `WriteResult` (retired) | `BatchHandler = (batch, context) -> None` — the only outlet contract |
|
|
127
|
+
| `RecordHandler` alias | `BatchHandler` |
|
|
128
|
+
| — | `Probe = (record) -> None` (single-record probe for precise DLQ location) |
|
|
129
|
+
| `ConsumerConfig` (deleted) | flat arguments + `ConsumerOptions.tuning` / `options.dlq` |
|
|
130
|
+
| `ConsumeSpec.expected_message_type` | `ConsumerOptions.expected_type` |
|
|
131
|
+
| `ConsumeSpec.persist_policy` | `ConsumerOptions.persist_hook` |
|
|
132
|
+
| `ConsumeSpec.collapse_key` / `log_context` | `ConsumerOptions.collapse_key` / `log_context` |
|
|
133
|
+
| `ConsumeSpec.dlq` / `dlq_topic` / `dlq_message_type` | `ConsumerOptions.dlq.enabled` / `.topic` / `.message_type` |
|
|
134
|
+
| `ConsumerWorker.error_classifier` | `ConsumerOptions.classifier` |
|
|
135
|
+
| `ConsumerWorker.codec` | `ConsumerOptions.codec` |
|
|
136
|
+
| `ConsumerWorker.cache` | `ConsumerOptions.health_probe` |
|
|
137
|
+
| `ConsumerWorker.existence_ttl_seconds` | `ConsumerOptions.backlog_ttl_seconds` |
|
|
138
|
+
| `ConsumerWorker.metrics_config` | `ConsumerOptions.metrics_window_seconds` |
|
|
139
|
+
| `ConsumerConfig.batch_timeout_seconds` | `flush_timeout` argument / `CONSUMER__FLUSH_TIMEOUT_SECONDS` |
|
|
140
|
+
| `ConsumerConfig.reconnect_base_backoff_seconds` / `reconnect_max_backoff_seconds` | `RuntimeTuning.reconnect_base` / `.reconnect_max` |
|
|
141
|
+
| `ConsumerConfig.backlog_check_interval_seconds` | `RuntimeTuning.backlog_check_interval` |
|
|
142
|
+
| `KafkaConfig.dlq_topic` | `DlqOptions.topic` / env `KAFKA__DLQ_TOPIC` |
|
|
143
|
+
| `BisectOutcome.written` | `BisectOutcome.handled` |
|
|
144
|
+
| `locate_and_write(writer, ...)` | `locate_and_quarantine(probe, ...)` |
|
|
145
|
+
| `DlqProducer(kafka_config, ...)` | `DlqProducer(bootstrap_servers, topic=..., send_retries=..., ...)` |
|
|
146
|
+
| `KafkaConsumerService(kafka_config, consumer_config, topic)` | `KafkaConsumerService(bootstrap_servers, topic, group_id, auto_offset_reset=..., ...)` |
|
|
147
|
+
|
|
148
|
+
API renames / removals (producer side):
|
|
149
|
+
|
|
150
|
+
| 0.x | 1.0.0 |
|
|
151
|
+
|-----|-------|
|
|
152
|
+
| `IngestBinding` (deleted) | flat `Producer` constructor arguments + `ProducerOptions` |
|
|
153
|
+
| `IngestGateway(binding=...)` | `Producer(bootstrap_servers, topic, key, options)` |
|
|
154
|
+
| `IngestGateway.process()` | `Producer.push()` |
|
|
155
|
+
| `IngestOutcome` / `OutcomeKind` | `PushResult` / `PushKind` |
|
|
156
|
+
| `OutcomeKind.CONFLICT` | `PushKind.DUPLICATE` |
|
|
157
|
+
| `OutcomeKind.KAFKA_UNAVAILABLE` | `PushKind.UNAVAILABLE` |
|
|
158
|
+
| `IngestGateway.process(overwrite=True)` / `IngestBinding.is_overwrite` | `Producer.push(force=True)` (per-call, not per-record) |
|
|
159
|
+
| `IngestBinding.entity_key` + `slot_key` | split: `Producer(key=...)` = routing key (ordering); `DedupOptions(key=...)` = identity key (dedup) |
|
|
160
|
+
| `IngestBinding.partition_key` | `Producer(key=...)` |
|
|
161
|
+
| `IngestBinding.message_type` | `ProducerOptions.message_type` (default `streamgate_record`) |
|
|
162
|
+
| `IngestBinding.summary` | `DedupOptions.summary` |
|
|
163
|
+
| `IngestBinding.admission="in-memory"` | `ProducerOptions(dedup=DedupOptions(key=...))` (built-in in-process carrier) |
|
|
164
|
+
| `IngestBinding.admission=<AdmissionPolicy>` | `DedupOptions(carrier=<DedupCarrier>)` |
|
|
165
|
+
| `IngestBinding.log_context` | `ProducerOptions.log_context` |
|
|
166
|
+
| `IngestBinding.request_log_context` | removed (HTTP presentation contract — map it in your adapter) |
|
|
167
|
+
| `IngestBinding.backpressure_error_codes` / `backpressure_default_code` / `backpressure_detail` | removed (core only returns `kind`; HTTP mapping is yours) |
|
|
168
|
+
| `IngestBinding.kafka_unavailable_code` / `kafka_unavailable_detail` | removed (`PushResult.reason` only) |
|
|
169
|
+
| `IngestOutcome.status_code` / `error_code` / `detail` | removed — `PushResult` carries `kind` + `reason` + `retry_after` only |
|
|
170
|
+
| `IngestGateway.health()` → `IngestHealthResponse` | `Producer.health()` → `ProducerHealthResponse` |
|
|
171
|
+
| `AdmissionPolicy` protocol | `DedupCarrier` protocol (same hooks; `RejectInfo` carries `reason` + `retry_after`, no HTTP codes; `on_overwrite_accepted` renamed `on_force_accepted`; new `guarantee` property, self-reported) |
|
|
172
|
+
| `RejectInfo.status_code` / `error_code` / `detail` | removed — `RejectInfo(reason, retry_after, ...)` |
|
|
173
|
+
| `DecisionKind.CONFLICT` | `DecisionKind.DUPLICATE` |
|
|
174
|
+
| `InMemoryAdmission` / `NoAdmission` | built-in `InMemoryDedupCarrier` (auto-selected); `dedup=None` replaces `NoAdmission` |
|
|
175
|
+
| `KafkaProducerService` | internalized into `Producer` (self-healing preserved, class no longer exported) |
|
|
176
|
+
| `streamgate.contrib.redis_admission` | `streamgate.contrib.redis_dedup` |
|
|
177
|
+
| `RedisExistenceAdmission` | `RedisDedupCarrier` (`guarantee="distributed"`; `entity_key`/`slot_key` → single `key`; query endpoint `entity_slots` removed) |
|
|
178
|
+
| `RedisExistenceAdmissionConfig` | `RedisDedupCarrierConfig` (error-code fields removed) |
|
|
179
|
+
| `RedisExistenceCache` | `RedisDedupCache` (identity→summary STRING keys, single-key Lua reserve) |
|
|
180
|
+
| `RedisConfig.existence_ttl_seconds` | `RedisConfig.identity_ttl_seconds` (empty-entity sentinel removed — no entity grouping) |
|
|
181
|
+
| `BackfillSource.load(entity) -> dict[slot, summary]` | `BackfillSource.load(identity) -> summary \| None` |
|
|
182
|
+
| `SqlBackfill(entity_column=..., slot_column=...)` | `SqlBackfill(key_column=...)` (single identity key) |
|
|
183
|
+
| `IngestMetrics` | `ProducerMetrics` (`admission_conflict` → `duplicate`) |
|
|
184
|
+
| `collect_ingest_health` | `collect_producer_health` |
|
|
185
|
+
|
|
186
|
+
Environment variables:
|
|
187
|
+
|
|
188
|
+
| 0.x | 1.0.0 |
|
|
189
|
+
|-----|-------|
|
|
190
|
+
| `CONSUMER__BATCH_TIMEOUT_SECONDS` | renamed to `CONSUMER__FLUSH_TIMEOUT_SECONDS` |
|
|
191
|
+
| all other `KAFKA__*` / `CONSUMER__*` / `METRICS__*` keys | unchanged (now read directly by `Producer` / `Consumer` as fallbacks: `KAFKA__BOOTSTRAP_SERVERS`, `KAFKA__TOPIC`, `CONSUMER__GROUP_ID`, `CONSUMER__BATCH_SIZE`, DLQ, tuning and metrics-window keys) |
|
|
192
|
+
|
|
193
|
+
Health-snapshot metrics:
|
|
194
|
+
|
|
195
|
+
| 0.x | 1.0.0 |
|
|
196
|
+
|-----|-------|
|
|
197
|
+
| `ConsumerHealthResponse.database` | `output` (outlet connectivity; outlet without an observable carrier reads `connected`) |
|
|
198
|
+
| `sink_write_rate` | `handle_rate` |
|
|
199
|
+
| `sink_write_failure_rate` | `handle_failure_rate` |
|
|
200
|
+
| `sink_write_latency_ms_avg` / `_max` | `handle_latency_ms_avg` / `_max` |
|
|
201
|
+
| `consume_rate` / `retry_rate` | unchanged |
|
|
202
|
+
| `IngestHealthResponse.receive_rate` | `ProducerHealthResponse.push_rate` |
|
|
203
|
+
| `IngestHealthResponse.admission_conflict_rate` | `ProducerHealthResponse.duplicate_rate` |
|
|
204
|
+
|
|
205
|
+
Log events (both sides keep stable names; producer-side field renames only):
|
|
206
|
+
|
|
207
|
+
| 0.x | 1.0.0 |
|
|
208
|
+
|-----|-------|
|
|
209
|
+
| `batch_write_start` | `batch_handle_start` |
|
|
210
|
+
| `batch_write_success` | `batch_handle_success` |
|
|
211
|
+
| `batch_write_failed` | `batch_handle_failed` |
|
|
212
|
+
| `batch_write_exhausted_retries` | `batch_handle_exhausted_retries` |
|
|
213
|
+
| `batch_write_success_with_quarantine` | `batch_handle_success_with_quarantine` |
|
|
214
|
+
| `consumer_paused_due_to_write_failures` | `consumer_paused_due_to_handle_failures` |
|
|
215
|
+
| `offset_commit_failed_after_write` | `offset_commit_failed_after_handle` |
|
|
216
|
+
| `write_failure_classified` | `handle_failure_classified` |
|
|
217
|
+
| `batch_bisect_triggered` / `bisect_aborted_probe_failed` / `consumer_record_quarantined` | unchanged |
|
|
218
|
+
| `existence_ttl_seconds` log field | `backlog_ttl_seconds` |
|
|
219
|
+
| `ingest_request` field `overwrite` | field `force` (event name unchanged) |
|
|
220
|
+
| `admission_send_success_hook_failed` / `admission_send_failed_hook_failed` | `dedup_send_success_hook_failed` / `dedup_send_failed_hook_failed` |
|
|
221
|
+
| `overwrite_rejected_redis_unavailable` / `overwrite_summary_write_failed` | `force_rejected_redis_unavailable` / `force_summary_write_failed` |
|
|
222
|
+
| `load_entity_failed` / `load_entity_failed_degraded` / `cold_path_db_load` | `load_identity_failed` / `load_identity_failed_degraded` / `cold_path_load` |
|
|
223
|
+
| `ingest_request` / `ingest_conflict` / `ingest_rejected_backpressure` / `kafka_*` | unchanged (the `error_code` field was dropped from push-path rejections) |
|
|
224
|
+
|
|
225
|
+
Import paths:
|
|
226
|
+
|
|
227
|
+
| 0.x | 1.0.0 |
|
|
228
|
+
|-----|-------|
|
|
229
|
+
| `streamgate.contrib.sql_sink` | `streamgate.contrib.sql_upsert` (base: `Upsert`, `upsert_outlet`, engine factory, `SqlBackfill`, `SQLAlchemyErrorClassifier`, `DbConfig`) |
|
|
230
|
+
| `streamgate.contrib.sqlite_sink` | `streamgate.contrib.sqlite_upsert` (exports `SqliteConsumer` factory) |
|
|
231
|
+
| `streamgate.contrib.mssql_sink` | `streamgate.contrib.mssql_upsert` (exports `MssqlConsumer` factory) |
|
|
232
|
+
| `UpsertWriter` (retired) | `upsert_outlet(db, upserts) -> tuple[BatchHandler, Probe]` |
|
|
233
|
+
| `streamgate.ingest.gateway` / `streamgate.specs` / `streamgate.ingest.admission` | deleted — use `streamgate.Producer` / `ProducerOptions` / `DedupOptions` |
|
|
234
|
+
| `streamgate.contrib.redis_admission` | `streamgate.contrib.redis_dedup` |
|
|
235
|
+
|
|
236
|
+
No compatibility shims are provided (consistent with a major boundary);
|
|
237
|
+
passing a retired keyword to `Consumer` or `Producer` raises a `TypeError`
|
|
238
|
+
that points to this section.
|
|
239
|
+
|
|
240
|
+
### Added
|
|
241
|
+
|
|
242
|
+
- **`Consumer`** — the flat, embeddable consume loop: four required arguments
|
|
243
|
+
answer "which cluster / which topic / which identity / where data goes";
|
|
244
|
+
`batch_size` (default 500) and `flush_timeout` (default 5.0s) are
|
|
245
|
+
first-class; everything else lives in `ConsumerOptions` (with nested
|
|
246
|
+
`DlqOptions` and `RuntimeTuning`) and defaults to zero conceptual load.
|
|
247
|
+
Required settings fall back to environment variables (12-factor):
|
|
248
|
+
`KAFKA__BOOTSTRAP_SERVERS` / `KAFKA__TOPIC` / `CONSUMER__GROUP_ID` (the
|
|
249
|
+
handler is code-only by design).
|
|
250
|
+
- **`Producer`** — the flat, embeddable push entry: two required arguments
|
|
251
|
+
(`bootstrap_servers`, `topic`, both with `KAFKA__*` env fallbacks) plus an
|
|
252
|
+
optional routing `key` (same key ⇒ same partition ⇒ ordered; omitted ⇒
|
|
253
|
+
round-robin, unordered — documented promise). `push(record, force=False,
|
|
254
|
+
source=...)` returns a transport-neutral `PushResult` (`accepted` /
|
|
255
|
+
`duplicate` / `rejected` / `backpressure` / `unavailable`) and never raises
|
|
256
|
+
runtime exceptions for degraded dependencies. Dedup is opt-in via
|
|
257
|
+
`ProducerOptions(dedup=DedupOptions(key=...))` — one line enables the
|
|
258
|
+
built-in in-process carrier (`guarantee="process-local"`); injecting a
|
|
259
|
+
`DedupCarrier` (e.g. `contrib.redis_dedup.RedisDedupCarrier`,
|
|
260
|
+
`guarantee="distributed"`) scales the guarantee with your topology. The
|
|
261
|
+
carrier self-reports its guarantee strength and it surfaces as
|
|
262
|
+
`PushResult.guarantee` on every result. Rejection windows mean zero writes:
|
|
263
|
+
no reservation, no shared-store write, no Kafka send. The Kafka
|
|
264
|
+
connection's self-healing monitor (probing, single-flight rebuild,
|
|
265
|
+
exponential backoff) is preserved internally — no separate class to manage.
|
|
266
|
+
- **Single-record probe (`ConsumerOptions.probe`)** — neutral primitive for
|
|
267
|
+
precise poison isolation: when a handler raises a POISON-classified error
|
|
268
|
+
and a probe is provided, the framework re-runs the probe per record
|
|
269
|
+
(with reference comparison to distinguish data poison from outlet
|
|
270
|
+
failure); probe-successful records count as handled and commit, probe-
|
|
271
|
+
failing records are quarantined to the DLQ individually. Without a probe
|
|
272
|
+
the whole batch is quarantined (offsets still commit, no poison retries).
|
|
273
|
+
With the DLQ disabled the legacy paused self-healing applies.
|
|
274
|
+
- **`SqliteConsumer` / `MssqlConsumer` factories** — pre-assembled `Consumer`s
|
|
275
|
+
for the SQL outlet: batch upsert handler + single-record probe + dialect
|
|
276
|
+
error classifier wired, user `options` merged field-by-field over factory
|
|
277
|
+
defaults. They return the core `Consumer` (no subclass hierarchy). SQLite
|
|
278
|
+
auto-creates tables at start (unchanged); MSSQL schema stays with your
|
|
279
|
+
migration tool.
|
|
280
|
+
- Old parameter names passed to `Consumer` or `Producer` fail with a
|
|
281
|
+
`TypeError` carrying a migration hint.
|
|
282
|
+
|
|
283
|
+
### Changed
|
|
284
|
+
|
|
285
|
+
- Poison handling is probe-based instead of write-batch bisection: location
|
|
286
|
+
calls the user probe record-by-record with reference verification; the
|
|
287
|
+
safety invariants are unchanged (probe-failed entries are never
|
|
288
|
+
quarantined; a suspected outlet failure aborts location and pauses —
|
|
289
|
+
handled entries are idempotently re-processed on the next pass).
|
|
290
|
+
- `ConsumerHealthResponse` / `ConsumeMetrics` vocabulary neutralized to
|
|
291
|
+
handle-rate/handle-latency; the producer side follows:
|
|
292
|
+
`IngestHealthResponse` → `ProducerHealthResponse`,
|
|
293
|
+
`receive_rate` → `push_rate`, `admission_conflict_rate` → `duplicate_rate`
|
|
294
|
+
(mapping tables above).
|
|
295
|
+
- Handler objects (callables implementing `__call__`) may opt into framework-
|
|
296
|
+
managed lifecycle via duck-typed `start()` / `close()` and into the health
|
|
297
|
+
snapshot via `check_health()` (used by the SQL outlet to report
|
|
298
|
+
`output` connectivity).
|
|
299
|
+
- Dedup carriers and backpressure signals implementing `start()`/`close()`
|
|
300
|
+
get framework-managed lifecycles (same duck-typed contract as consumer
|
|
301
|
+
hooks).
|
|
302
|
+
|
|
303
|
+
### Removed
|
|
304
|
+
|
|
305
|
+
- `ConsumeSpec`, `RecordWriter`, `WriteResult`, `RecordHandler`,
|
|
306
|
+
`ConsumerWorker`, `ConsumerConfig`, `KafkaConfig.dlq_topic`,
|
|
307
|
+
`contrib.sql_sink` / `contrib.sqlite_sink` / `contrib.mssql_sink`
|
|
308
|
+
(renamed — see import mapping above).
|
|
309
|
+
- Producer side: `IngestBinding`, `IngestGateway`, `IngestOutcome`,
|
|
310
|
+
`OutcomeKind`, `KafkaProducerService`, `InMemoryAdmission`, `NoAdmission`,
|
|
311
|
+
the `streamgate.ingest.admission` package, `streamgate.specs`,
|
|
312
|
+
`IngestRecordT`, and the HTTP-presentation contracts
|
|
313
|
+
(`status_code` / `error_code` / `detail` fields on results and rejects,
|
|
314
|
+
`backpressure_error_codes` / `backpressure_default_code` /
|
|
315
|
+
`backpressure_detail` / `kafka_unavailable_code` /
|
|
316
|
+
`kafka_unavailable_detail` / `request_log_context`).
|
|
317
|
+
- `contrib.redis_admission` (renamed to `contrib.redis_dedup` — see import
|
|
318
|
+
mapping above; the per-entity query endpoint `entity_slots` and the
|
|
319
|
+
empty-entity sentinel were not carried over: the single-identity-key model
|
|
320
|
+
has no entity grouping).
|
|
321
|
+
|
|
322
|
+
## [0.5.0] - 2026-09-10
|
|
323
|
+
|
|
324
|
+
### Added
|
|
325
|
+
|
|
326
|
+
- **`AdmissionPolicy` gains `on_send_success` / `on_send_failed` hooks.**
|
|
327
|
+
`on_send_success(record)` fires after every broker-acked Kafka send
|
|
328
|
+
(overwrite path included); `on_send_failed(record)` fires when a send
|
|
329
|
+
fails after the admission reservation was placed. The framework default
|
|
330
|
+
(no-op) preserves current behavior — the placeholder stays reserved and
|
|
331
|
+
self-heals via TTL/backfill — but implementations can now release the
|
|
332
|
+
reservation to make the key immediately re-submittable, accepting the
|
|
333
|
+
duplicate risk of ambiguous (timeout) failures.
|
|
334
|
+
|
|
335
|
+
### Changed
|
|
336
|
+
|
|
337
|
+
- **`AdmissionPolicy.on_accepted` renamed to `on_overwrite_accepted`.**
|
|
338
|
+
The old hook only fired on the overwrite path (not on every accepted
|
|
339
|
+
send, despite the name); the new name states that. Custom policies that
|
|
340
|
+
implement `on_accepted` keep working: the gateway falls back to it when
|
|
341
|
+
`on_overwrite_accepted` is absent. Built-in policies provide both
|
|
342
|
+
(the old name delegates to the new one).
|
|
343
|
+
|
|
344
|
+
## [0.4.0] - 2026-09-08
|
|
345
|
+
|
|
346
|
+
### Added
|
|
347
|
+
|
|
348
|
+
- **Health snapshots now carry ops rate/latency metrics.** Both
|
|
349
|
+
`IngestHealthResponse` and `ConsumerHealthResponse` gain sliding-window
|
|
350
|
+
fields (all defaulting to `0.0`, computed on read over a 1-second bucket
|
|
351
|
+
sliding window — no background tasks, no extra endpoint):
|
|
352
|
+
- ingest: `receive_rate`, `admission_conflict_rate`,
|
|
353
|
+
`backpressure_reject_rate`, `produce_success_rate`,
|
|
354
|
+
`produce_failure_rate`, `produce_latency_ms_avg`,
|
|
355
|
+
`produce_latency_ms_max`;
|
|
356
|
+
- consume: `consume_rate`, `sink_write_rate`, `sink_write_failure_rate`,
|
|
357
|
+
`retry_rate`, `sink_write_latency_ms_avg`, `sink_write_latency_ms_max`.
|
|
358
|
+
Rates are records/second, latencies milliseconds; the window length is
|
|
359
|
+
configurable via the new `MetricsConfig` (`METRICS__WINDOW_SECONDS`,
|
|
360
|
+
default `60`, range 1–600). `IngestGateway(metrics_config=...)` and
|
|
361
|
+
`ConsumerWorker(metrics_config=...)` accept it; when omitted everything
|
|
362
|
+
works with the default window. Purely additive — old consumers that
|
|
363
|
+
deserialize the snapshot ignore the new fields.
|
|
364
|
+
- **`MetricsConfig`** exported from the package root.
|
|
365
|
+
|
|
366
|
+
### Changed
|
|
367
|
+
|
|
368
|
+
- `ConsumeRuntime.__init__`: the structured-metrics sink parameter
|
|
369
|
+
`metrics` was renamed to `metrics_sink` to free the `metrics` name for
|
|
370
|
+
the new rate-metrics container (`ConsumeMetrics`). No in-repo caller
|
|
371
|
+
passed it explicitly; external callers using `metrics=<MetricsSink>`
|
|
372
|
+
must rename the keyword.
|
|
373
|
+
|
|
374
|
+
## [0.3.1] - 2026-09-08
|
|
375
|
+
|
|
376
|
+
### Fixed
|
|
377
|
+
|
|
378
|
+
- **`ConsumerWorker` now manages the `ConsumeSpec.persist_policy` lifecycle**
|
|
379
|
+
(the `AdmissionPolicy` protocol promises "the framework guarantees call
|
|
380
|
+
ordering", but the consumer entry point never honored it — unlike
|
|
381
|
+
`IngestGateway`, which calls `start()`/`close()`). `prepare()` now calls
|
|
382
|
+
`await policy.start()` before starting the sink, and `shutdown()` calls
|
|
383
|
+
`await policy.close()` before closing the writer. Previously the consumer
|
|
384
|
+
side never started the policy, so `on_persisted` authoritative refresh /
|
|
385
|
+
TTL heartbeats failed on every batch (e.g. `RedisExistenceAdmission`
|
|
386
|
+
logging `cache_refresh_failed: RedisExistenceCache not started`) and the
|
|
387
|
+
failure surfaced only as per-record WARN logs. Lifecycle calls are
|
|
388
|
+
duck-typed (defensive `getattr`) so legacy policy implementations without
|
|
389
|
+
`start()`/`close()` keep working.
|
|
390
|
+
- **`ConsumerWorker(cache=...)` lifecycle is now symmetric.** The bypass
|
|
391
|
+
`cache` parameter (health-probe component) is started in `prepare()` when
|
|
392
|
+
it implements `start()`, instead of being closed at shutdown without ever
|
|
393
|
+
having been started. `RedisExistenceCache.start()`/`close()` are
|
|
394
|
+
idempotent, so sharing one cache instance between the policy and the
|
|
395
|
+
bypass parameter remains safe (double start/close is a no-op).
|
|
396
|
+
The pre-fix workaround (`await cache.start()` before `worker.run()`)
|
|
397
|
+
is no longer required.
|
|
398
|
+
|
|
399
|
+
## [0.3.0] - 2026-09-08
|
|
400
|
+
|
|
401
|
+
**Contrib release.** The production-grade I/O strategies that 0.2.0 demoted to
|
|
402
|
+
copy-paste examples are back in the wheel — as `streamgate.contrib.*`,
|
|
403
|
+
importable, opt-in subpackages. The core stays pure: it gains no dependencies,
|
|
404
|
+
and it still never imports contrib (the import-linter forbidden contract is
|
|
405
|
+
replaced by a layered contract with the same guarantee). Purely additive; no
|
|
406
|
+
breaking changes.
|
|
407
|
+
|
|
408
|
+
### Added
|
|
409
|
+
|
|
410
|
+
- **`streamgate.contrib`** — official strategy layer (provisional / beta
|
|
411
|
+
grade; ships in the wheel, third-party deps via extras, friendly
|
|
412
|
+
"install streamgate[...]" errors when an extra is missing):
|
|
413
|
+
- `contrib.redis_admission` (extra `[redis]`): `RedisExistenceAdmission`,
|
|
414
|
+
`RedisExistenceCache`, `RedisConfig` — shared-storage uniqueness
|
|
415
|
+
admission with atomic Lua reservation, TTL idle-GC, empty-entity
|
|
416
|
+
sentinels, fail-closed semantics.
|
|
417
|
+
- `contrib.sql_sink` / `contrib.sqlite_sink` / `contrib.mssql_sink`
|
|
418
|
+
(extra `[sql]`): `Upsert`/`UpsertWriter` (`RecordWriter` impl),
|
|
419
|
+
engine factory, `SqlBackfill` (`BackfillSource` impl),
|
|
420
|
+
`SQLAlchemyErrorClassifier`, `DbConfig` — SQLite `ON CONFLICT` and
|
|
421
|
+
MSSQL multi-row `MERGE` + HOLDLOCK dialects.
|
|
422
|
+
- `contrib.http_probe` (extra `[http]`): `HttpProbeSignal` /
|
|
423
|
+
`HysteresisController` — backlog-age trip/recover hysteresis,
|
|
424
|
+
fail-closed probing, kafka-down fast reject.
|
|
425
|
+
- **Extras** `[redis]` / `[sql]` / `[http]` (new names; the pre-0.2.0 extras
|
|
426
|
+
`[sqlite]` / `[mssql]` / `[http-probe]` / `[all]` are not restored).
|
|
427
|
+
- **`examples/prod_pipeline/`** — production-topology demo wiring the three
|
|
428
|
+
contrib components together (Redis uniqueness admission + HTTP-probe
|
|
429
|
+
backpressure + MSSQL idempotent sink; runs on SQLite locally via
|
|
430
|
+
connection-string dialect selection).
|
|
431
|
+
|
|
432
|
+
### Changed
|
|
433
|
+
|
|
434
|
+
- **import-linter**: the "pure core" forbidden-modules contract is replaced
|
|
435
|
+
by a layered contract — `streamgate.contrib` sits on top and may use the
|
|
436
|
+
core; no core layer may import contrib. Bare-install purity is still
|
|
437
|
+
CI-gated (bare install pulls exactly `aiokafka`/`loguru`/`pydantic`).
|
|
438
|
+
- **`examples/`**: the three strategy examples now import from
|
|
439
|
+
`streamgate.contrib`; demo scripts (produce/consume/health_server/models)
|
|
440
|
+
stay in the repo only. `pure_pipeline/` remains the zero-dependency main
|
|
441
|
+
example and is unchanged.
|
|
442
|
+
- **Dev group** now depends on `streamgate[redis,sql,http]` instead of
|
|
443
|
+
duplicating the third-party libs, keeping a single source of truth for
|
|
444
|
+
contrib dependency versions.
|
|
445
|
+
|
|
446
|
+
## [0.2.0] - 2026-09-07
|
|
447
|
+
|
|
448
|
+
**Pure-core release.** All database, Redis and HTTP-client code is removed from
|
|
449
|
+
the wheel; every I/O strategy is now a typed protocol hook with a copy-paste
|
|
450
|
+
reference implementation in `examples/`. Core dependencies collapse to
|
|
451
|
+
`aiokafka` + `loguru` + `pydantic`. This is a breaking release (0.1.0 was
|
|
452
|
+
published days earlier; the migration cost is intentionally accepted).
|
|
453
|
+
|
|
454
|
+
### Changed
|
|
455
|
+
|
|
456
|
+
- **Core dependencies**: `aiokafka` / `loguru` / `pydantic` only. The
|
|
457
|
+
`sqlalchemy` and `sqlmodel` dependencies — and the entire extras system
|
|
458
|
+
(`[sqlite]`, `[mssql]`, `[redis]`, `[http-probe]`, `[all]`) — are gone.
|
|
459
|
+
- **`ConsumeSpec`**: landing targets reduced to two, mutually exclusive:
|
|
460
|
+
`sink` (a `RecordWriter` — the only persistence path) or `on_record`
|
|
461
|
+
(escape hatch). Providing neither — or both — fails fast at declaration
|
|
462
|
+
with fix guidance.
|
|
463
|
+
- **`IngestBinding.admission`**: string shortcuts are now `"none"` and
|
|
464
|
+
`"in-memory"` (new built-in, zero-I/O, single-process). `"redis-existence"`
|
|
465
|
+
is removed — inject a `RedisExistenceAdmission` instance instead
|
|
466
|
+
(copy from `examples/redis_admission/`).
|
|
467
|
+
- **`IngestGateway`**: default backpressure signal is now the built-in
|
|
468
|
+
`ManualBackpressureSignal` (static switch, defaults to not rejecting);
|
|
469
|
+
`db_config` / `redis_config` constructor parameters removed.
|
|
470
|
+
- **`ConsumerWorker`**: constructor parameters `db_config`, `redis_config`,
|
|
471
|
+
`writer`, `engine`, `session_factory` removed — the sink comes from
|
|
472
|
+
`ConsumeSpec.sink`. New `error_classifier` parameter (see below).
|
|
473
|
+
|
|
474
|
+
### Added
|
|
475
|
+
|
|
476
|
+
- **`ErrorClassifier` protocol + `ErrorKind` enum** (`RETRY` / `POISON` /
|
|
477
|
+
`FATAL`): write-failure classification is now an injected policy.
|
|
478
|
+
`DefaultErrorClassifier` only recognizes generic exceptions
|
|
479
|
+
(timeouts / KafkaError → retry; ValueError → poison; unknown → poison,
|
|
480
|
+
probe-protected bisect). Consumers writing to a database **must** inject a
|
|
481
|
+
DB-aware classifier — `examples/sqlite_sink/classifier.py` ships a
|
|
482
|
+
production-hardened `SQLAlchemyErrorClassifier` (incl. SQL Server error
|
|
483
|
+
numbers).
|
|
484
|
+
- **`ManualBackpressureSignal`**: built-in pure in-memory backpressure switch.
|
|
485
|
+
- **`InMemoryAdmission`**: built-in zero-I/O uniqueness admission
|
|
486
|
+
(single-process only; multi-instance topologies should inject a
|
|
487
|
+
shared-storage policy).
|
|
488
|
+
- **`examples/`** (not part of the wheel, CI-gated for lint/typecheck):
|
|
489
|
+
- `pure_pipeline/` — bare-install full round trip (main example; the
|
|
490
|
+
living proof of `pip install streamgate` purity)
|
|
491
|
+
- `sqlite_sink/` — the former `db/` package (engines / upsert sugar /
|
|
492
|
+
backfill / SQLite+MSSQL dialects) as local modules + `RecordWriter`
|
|
493
|
+
injection + `SQLAlchemyErrorClassifier`
|
|
494
|
+
- `redis_admission/` — the former `cache/existence.py` +
|
|
495
|
+
`ingest/admission/redis_existence.py` + `AdmissionPolicy` injection
|
|
496
|
+
- `http_probe/` — the former `HttpProbeSignal` (hysteresis state machine) +
|
|
497
|
+
`BackpressureSignal` injection
|
|
498
|
+
|
|
499
|
+
### Removed
|
|
500
|
+
|
|
501
|
+
- `streamgate.db` package (`UpsertWriter`, `SqlBackfill`, engine factories,
|
|
502
|
+
SQLite/MSSQL dialects), `Upsert` declaration, `DbConfig` / `RedisConfig`
|
|
503
|
+
(and their `DB__*` / `REDIS__*` env mappings), `RedisExistenceCache`,
|
|
504
|
+
`RedisExistenceAdmission` (+ related symbols), `HttpProbeSignal`,
|
|
505
|
+
`_optional` lazy-loading gate.
|
|
506
|
+
- `FailureCategory` / `add_failure_rule` / `classify_write_failure`
|
|
507
|
+
(superseded by `ErrorClassifier`).
|
|
508
|
+
|
|
509
|
+
### Migration Guide (0.1.0 → 0.2.0)
|
|
510
|
+
|
|
511
|
+
| 0.1.0 symbol | Where it went |
|
|
512
|
+
|--------------|---------------|
|
|
513
|
+
| `Upsert` | `examples/sqlite_sink/upsert.py` (`Upsert`, local class) |
|
|
514
|
+
| `UpsertWriter` | `examples/sqlite_sink/upsert.py` — inject via `ConsumeSpec(sink=UpsertWriter(...))` |
|
|
515
|
+
| `DbConfig`, `create_read_engine`, `create_write_engine`, `async_session_factory` | `examples/sqlite_sink/config.py` + `engines.py` |
|
|
516
|
+
| `SqlBackfill` | `examples/sqlite_sink/backfill.py` |
|
|
517
|
+
| SQLite / MSSQL dialect internals | `examples/sqlite_sink/dialects/` |
|
|
518
|
+
| `RedisExistenceCache`, `EMPTY_FIELD` | `examples/redis_admission/existence.py` |
|
|
519
|
+
| `RedisExistenceAdmission`, `RedisExistenceAdmissionConfig`, `EntitySlots`, `SlotSource`, `ExistenceUnavailableError`, `UndeterminedReason` | `examples/redis_admission/admission.py` — inject via `IngestBinding(admission=RedisExistenceAdmission(...))` |
|
|
520
|
+
| `HttpProbeSignal` | `examples/http_probe/probe_signal.py` — inject via `IngestGateway(signal=HttpProbeSignal(...))` |
|
|
521
|
+
| `FailureCategory`, `classify_write_failure`, `add_failure_rule` | `ErrorKind` + `ErrorClassifier`; DB mapping in `examples/sqlite_sink/classifier.py` |
|
|
522
|
+
| `ConsumeSpec(upserts=[...])` | `ConsumeSpec(sink=UpsertWriter(..., [Upsert(...)]))` (writer built by you) |
|
|
523
|
+
| `ConsumerWorker(db_config=..., spec=ConsumeSpec(upserts=...))` | `ConsumerWorker(spec, ...)` with `ConsumeSpec(sink=...)`; pass `error_classifier=` |
|
|
524
|
+
| `admission="redis-existence"` | `admission=RedisExistenceAdmission(...)` (instance injection) |
|
|
525
|
+
| `pip install "streamgate[redis]"` etc. | `pip install streamgate <third-party-lib>` + copy the example module |
|
|
526
|
+
|
|
527
|
+
## [0.1.0] - 2026-09-07
|
|
528
|
+
|
|
529
|
+
First public release. Data pipeline framework: conditional admission → reliable delivery → pluggable sinks.
|
|
530
|
+
|
|
531
|
+
### Added
|
|
532
|
+
|
|
533
|
+
- **Ingest (admission)**
|
|
534
|
+
- `IngestGateway`: transport-agnostic ingest kernel — backpressure check → admission → Kafka send → on_accepted orchestration.
|
|
535
|
+
- `IngestBinding`: declarative ingest binding (Tier 0) with `admission="none" | "redis-existence"` shortcuts or a custom `AdmissionPolicy`.
|
|
536
|
+
- `RedisExistenceAdmission` / `RedisExistenceCache` (extras: `[redis]`): uniqueness contract via existence check + atomic slot reservation (cluster-safe Lua) + TTL idle-GC + empty-entity sentinels + fail-closed semantics; optional cold-entity DB backfill via `BackfillSource` (`SqlBackfill`).
|
|
537
|
+
- **Transport**
|
|
538
|
+
- `KafkaProducerService` / `KafkaConsumerService` over aiokafka: lazy connect, health probing, self-healing reconnects with exponential backoff and rebuild thresholds.
|
|
539
|
+
- `JsonEnvelopeCodec` envelope format; custom `MessageCodec` supported.
|
|
540
|
+
- **Consumer (sink)**
|
|
541
|
+
- `ConsumerWorker` + `consume_loop`: batch buffering, at-least-once processing, graceful shutdown.
|
|
542
|
+
- `ConsumeSpec` (Tier 0) with three mutually exclusive landing targets: `upserts` (built-in idempotent SQL upserts via `Upsert`), `sink` (`RecordWriter` injection point), `on_record` (no-sink escape hatch).
|
|
543
|
+
- Built-in SQLite / SQL Server upsert dialects (extras: `[sqlite]` / `[mssql]`); other databases via Tier 1 `RecordWriter`.
|
|
544
|
+
- DLQ bisection: poison records are located by batch bisection and quarantined to a dead-letter topic instead of wedging the partition.
|
|
545
|
+
- **Resilience & observability**
|
|
546
|
+
- Consumption backpressure with hysteresis (trip/recover on backlog age), fail-closed probing, `HttpProbeSignal` (extras: `[http-probe]`); custom `BackpressureSignal` supported.
|
|
547
|
+
- Health snapshots for both processes (`collect_ingest_health` / `collect_consumer_health`); exposure is left to the caller (the package contains no web-framework code).
|
|
548
|
+
- Structured logging via loguru (`configure_logger`, `logger`) and a pluggable `MetricsSink`.
|
|
549
|
+
- **Packaging**
|
|
550
|
+
- Mechanisms as core dependencies; all policy carriers (DB drivers, redis, httpx) as optional extras with lazy loading and install-guidance errors.
|
|
551
|
+
- Typed configuration objects mapping 1:1 to `KAFKA__*` / `CONSUMER__*` / `DB__*` / `REDIS__*` / `BACKPRESSURE__*` environment variables; required settings fail fast at startup.
|
|
552
|
+
|
|
553
|
+
[0.5.0]: https://github.com/pwg-code/streamgate/releases/tag/v0.5.0
|
|
554
|
+
[0.4.0]: https://github.com/pwg-code/streamgate/releases/tag/v0.4.0
|
|
555
|
+
[0.2.0]: https://github.com/pwg-code/streamgate/releases/tag/v0.2.0
|
|
556
|
+
[0.1.0]: https://github.com/pwg-code/streamgate/releases/tag/v0.1.0
|