natsio-pcgroups 0.1.0__py3-none-any.whl
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.
- natsio/pcgroups/__init__.py +147 -0
- natsio/pcgroups/buckets.py +45 -0
- natsio/pcgroups/elastic.py +561 -0
- natsio/pcgroups/entities.py +222 -0
- natsio/pcgroups/errors.py +65 -0
- natsio/pcgroups/partitions.py +170 -0
- natsio/pcgroups/py.typed +0 -0
- natsio/pcgroups/session.py +595 -0
- natsio/pcgroups/static.py +321 -0
- natsio_pcgroups-0.1.0.dist-info/METADATA +291 -0
- natsio_pcgroups-0.1.0.dist-info/RECORD +12 -0
- natsio_pcgroups-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Partitioned consumer groups over JetStream — static and elastic.
|
|
2
|
+
|
|
3
|
+
A consumer group splits a stream into ``max_members`` partitions keyed by
|
|
4
|
+
subject, and hands each *member* a fixed subset of them. Members consume in
|
|
5
|
+
parallel while messages that share a partitioning key are still processed one
|
|
6
|
+
at a time and in order — the throughput a single ``max_ack_pending=1`` consumer
|
|
7
|
+
cannot give you.
|
|
8
|
+
|
|
9
|
+
Two flavours, mirroring
|
|
10
|
+
[`orbit.go/pcgroups`](https://github.com/synadia-io/orbit.go/tree/main/pcgroups):
|
|
11
|
+
|
|
12
|
+
**Static** — the stream already carries the partition number as the first
|
|
13
|
+
subject token (put there by a stream subject transform on ingest). One durable
|
|
14
|
+
JetStream consumer per member, created straight on that stream. The membership
|
|
15
|
+
is fixed for the life of the group.
|
|
16
|
+
|
|
17
|
+
await create_static(js, "ORDERS", "cg", 10, ["orders.*"], ["m1", "m2"])
|
|
18
|
+
ctx = await static_consume(js, "ORDERS", "cg", "m1", handle)
|
|
19
|
+
|
|
20
|
+
**Elastic** — the stream needs no partition token. Creating the group creates a
|
|
21
|
+
sourced *work-queue* stream that inserts the partition token on the way in, and
|
|
22
|
+
the membership can change at any time; running members start and stop consuming
|
|
23
|
+
as they are added and dropped.
|
|
24
|
+
|
|
25
|
+
await create_elastic(js, "ORDERS", "cg", 10, [PartitioningFilter(filter="orders.*", partitioning_wildcards=[1])])
|
|
26
|
+
ctx = await elastic_consume(js, "ORDERS", "cg", "m1", handle)
|
|
27
|
+
await add_members(js, "ORDERS", "cg", ["m1", "m2"])
|
|
28
|
+
|
|
29
|
+
In both cases the handler is invoked with the partition token already stripped
|
|
30
|
+
from the subject, and running several instances of the same member gives you
|
|
31
|
+
hot-standby HA: the server's ADR-42 pinned-client priority group serves exactly
|
|
32
|
+
one instance at a time.
|
|
33
|
+
|
|
34
|
+
async def handle(msg: PartitionedMsg) -> None:
|
|
35
|
+
print(msg.subject, msg.data) # the subject as it was before partitioning
|
|
36
|
+
await msg.ack()
|
|
37
|
+
|
|
38
|
+
Requires nats-server 2.11+ (priority groups, subject transforms).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from natsio.pcgroups.elastic import (
|
|
42
|
+
ElasticConsumeContext,
|
|
43
|
+
add_members,
|
|
44
|
+
create_elastic,
|
|
45
|
+
delete_elastic,
|
|
46
|
+
delete_member_mappings,
|
|
47
|
+
delete_members,
|
|
48
|
+
elastic_consume,
|
|
49
|
+
elastic_is_in_membership_and_active,
|
|
50
|
+
elastic_member_step_down,
|
|
51
|
+
get_elastic_config,
|
|
52
|
+
list_elastic_active_members,
|
|
53
|
+
list_elastic_groups,
|
|
54
|
+
set_member_mappings,
|
|
55
|
+
)
|
|
56
|
+
from natsio.pcgroups.entities import (
|
|
57
|
+
DEFAULT_ACK_WAIT,
|
|
58
|
+
ELASTIC_BUCKET,
|
|
59
|
+
PRIORITY_GROUP,
|
|
60
|
+
STATIC_BUCKET,
|
|
61
|
+
ElasticConsumerGroupConfig,
|
|
62
|
+
MemberMapping,
|
|
63
|
+
PartitioningFilter,
|
|
64
|
+
StaticConsumerGroupConfig,
|
|
65
|
+
validate_elastic_config,
|
|
66
|
+
validate_static_config,
|
|
67
|
+
)
|
|
68
|
+
from natsio.pcgroups.errors import (
|
|
69
|
+
ConsumerGroupConfigError,
|
|
70
|
+
ConsumerGroupError,
|
|
71
|
+
ConsumerGroupExistsError,
|
|
72
|
+
ConsumerGroupNotFoundError,
|
|
73
|
+
GroupConfigChangedError,
|
|
74
|
+
MemberNotInGroupError,
|
|
75
|
+
)
|
|
76
|
+
from natsio.pcgroups.partitions import (
|
|
77
|
+
compose_group_stream_name,
|
|
78
|
+
compose_key,
|
|
79
|
+
compose_static_consumer_name,
|
|
80
|
+
elastic_get_partition_filters,
|
|
81
|
+
generate_partition_filters,
|
|
82
|
+
partitioning_transform_destination,
|
|
83
|
+
static_get_partition_filters,
|
|
84
|
+
strip_partition,
|
|
85
|
+
)
|
|
86
|
+
from natsio.pcgroups.session import ConsumerGroupConsumeContext, MessageHandler, PartitionedMsg
|
|
87
|
+
from natsio.pcgroups.static import (
|
|
88
|
+
StaticConsumeContext,
|
|
89
|
+
create_static,
|
|
90
|
+
delete_static,
|
|
91
|
+
get_static_config,
|
|
92
|
+
list_static_active_members,
|
|
93
|
+
list_static_groups,
|
|
94
|
+
static_consume,
|
|
95
|
+
static_member_step_down,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
__all__ = [
|
|
99
|
+
"DEFAULT_ACK_WAIT",
|
|
100
|
+
"ELASTIC_BUCKET",
|
|
101
|
+
"PRIORITY_GROUP",
|
|
102
|
+
"STATIC_BUCKET",
|
|
103
|
+
"ConsumerGroupConfigError",
|
|
104
|
+
"ConsumerGroupConsumeContext",
|
|
105
|
+
"ConsumerGroupError",
|
|
106
|
+
"ConsumerGroupExistsError",
|
|
107
|
+
"ConsumerGroupNotFoundError",
|
|
108
|
+
"ElasticConsumeContext",
|
|
109
|
+
"ElasticConsumerGroupConfig",
|
|
110
|
+
"GroupConfigChangedError",
|
|
111
|
+
"MemberMapping",
|
|
112
|
+
"MemberNotInGroupError",
|
|
113
|
+
"MessageHandler",
|
|
114
|
+
"PartitionedMsg",
|
|
115
|
+
"PartitioningFilter",
|
|
116
|
+
"StaticConsumeContext",
|
|
117
|
+
"StaticConsumerGroupConfig",
|
|
118
|
+
"add_members",
|
|
119
|
+
"compose_group_stream_name",
|
|
120
|
+
"compose_key",
|
|
121
|
+
"compose_static_consumer_name",
|
|
122
|
+
"create_elastic",
|
|
123
|
+
"create_static",
|
|
124
|
+
"delete_elastic",
|
|
125
|
+
"delete_member_mappings",
|
|
126
|
+
"delete_members",
|
|
127
|
+
"delete_static",
|
|
128
|
+
"elastic_consume",
|
|
129
|
+
"elastic_get_partition_filters",
|
|
130
|
+
"elastic_is_in_membership_and_active",
|
|
131
|
+
"elastic_member_step_down",
|
|
132
|
+
"generate_partition_filters",
|
|
133
|
+
"get_elastic_config",
|
|
134
|
+
"get_static_config",
|
|
135
|
+
"list_elastic_active_members",
|
|
136
|
+
"list_elastic_groups",
|
|
137
|
+
"list_static_active_members",
|
|
138
|
+
"list_static_groups",
|
|
139
|
+
"partitioning_transform_destination",
|
|
140
|
+
"set_member_mappings",
|
|
141
|
+
"static_consume",
|
|
142
|
+
"static_get_partition_filters",
|
|
143
|
+
"static_member_step_down",
|
|
144
|
+
"strip_partition",
|
|
145
|
+
"validate_elastic_config",
|
|
146
|
+
"validate_static_config",
|
|
147
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Access to the two KV buckets the group configs live in.
|
|
2
|
+
|
|
3
|
+
Both flavours store their configs in a KV bucket keyed ``"<stream>.<group>"``
|
|
4
|
+
(`STATIC_BUCKET` / `ELASTIC_BUCKET`), created on demand with the same shape the
|
|
5
|
+
oracle uses: file storage, replicated like the stream the groups sit on.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from natsio.jetstream import JetStreamContext, StorageType
|
|
9
|
+
from natsio.kv import KeyValue, KeyValueConfig
|
|
10
|
+
from natsio.kv.errors import BucketExistsError, BucketNotFoundError
|
|
11
|
+
|
|
12
|
+
from .errors import ConsumerGroupError, ConsumerGroupNotFoundError
|
|
13
|
+
|
|
14
|
+
__all__ = ["open_bucket", "open_or_create_bucket", "require_names"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def open_bucket(js: JetStreamContext, bucket: str) -> KeyValue:
|
|
18
|
+
"""Open a consumer-group KV bucket, mapping "no bucket" to "no group".
|
|
19
|
+
|
|
20
|
+
A missing bucket means no group of that flavour has ever been created in
|
|
21
|
+
this account, which is indistinguishable from "this group doesn't exist"
|
|
22
|
+
for every caller here.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
return await js.key_value(bucket)
|
|
26
|
+
except BucketNotFoundError as exc:
|
|
27
|
+
raise ConsumerGroupNotFoundError(f"the {bucket!r} KV bucket does not exist") from exc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def open_or_create_bucket(js: JetStreamContext, bucket: str, replicas: int) -> KeyValue:
|
|
31
|
+
"""Open a consumer-group KV bucket, creating it if it isn't there yet."""
|
|
32
|
+
try:
|
|
33
|
+
return await js.key_value(bucket)
|
|
34
|
+
except BucketNotFoundError:
|
|
35
|
+
pass
|
|
36
|
+
try:
|
|
37
|
+
return await js.create_key_value(KeyValueConfig(bucket=bucket, replicas=replicas, storage=StorageType.FILE))
|
|
38
|
+
except BucketExistsError:
|
|
39
|
+
return await js.key_value(bucket) # lost a creation race: theirs is as good as ours
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def require_names(stream: str, group: str) -> None:
|
|
43
|
+
"""Reject blank stream/group names before they become a malformed KV key."""
|
|
44
|
+
if not stream or not group:
|
|
45
|
+
raise ConsumerGroupError(f"invalid stream name {stream!r} or consumer group name {group!r}")
|