GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [connectors] NATS: use async-nats 0.51.0 · feldera/feldera@2520ef2

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • Cargo.lock
  • Cargo.toml
  • crates/connectors
    • Cargo.toml
    • src/transport/nats
      • input.rs
      • input/test
        • hardening_tests.rs
        • jetstream_proxy.rs
      • output.rs
  • docs.feldera.com/docs
    • changelog.md
    • connectors/sources
      • nats.md
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎Cargo.lock‎

Copy file name to clipboardExpand all lines: Cargo.lock
+10-8Lines changed: 10 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Collapse file

‎Cargo.toml‎

Copy file name to clipboardExpand all lines: Cargo.toml
+7-1Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,11 @@ async-channel = "2.3.1"
6767
# Its default feature set selects ring; aws-lc-rs is the same choice made
6868
# elsewhere in this workspace. The rest of the list restates the defaults, so
6969
# revisit it when upgrading.
70-
async-nats = { version = "0.47.0", default-features = false, features = [
70+
async-nats = { version = "0.51.0", default-features = false, features = [
7171
"server_2_10",
7272
"server_2_11",
7373
"server_2_12",
74+
"server_2_14",
7475
"service",
7576
"aws-lc-rs",
7677
"jetstream",
@@ -104,6 +105,11 @@ aws-lc-rs = "1.18.0"
104105
rsa = { version = "0.9.10", features = ["std"] }
105106
rcgen = { version = "0.14.8", default-features = false, features = ["pem", "aws_lc_rs"] }
106107
tokio-postgres-rustls = { version = "0.14.0", default-features = false, features = ["aws-lc-rs"] }
108+
# Pulled in by async-nats for ws:// server URLs. Its default hashes the
109+
# websocket handshake with sha1_smol; the feature keeps it on aws-lc-rs.
110+
# Keep the version in sync with the one async-nats depends on, or the
111+
# lockfile ends up with two copies and only async-nats's one is used.
112+
tokio-websockets = { version = "0.13.3", default-features = false, features = ["aws_lc_rs"] }
107113
base64 = "0.22.1"
108114
base58 = "0.2.0"
109115
binrw = "0.15.2"
Collapse file

‎crates/connectors/Cargo.toml‎

Copy file name to clipboardExpand all lines: crates/connectors/Cargo.toml
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ awc = { workspace = true, features = [
113113
"rustls-0_23-webpki-roots",
114114
] }
115115
async-nats = { workspace = true }
116+
# Only here to select aws-lc-rs for async-nats's websocket handshake.
117+
tokio-websockets = { workspace = true }
116118
async-stream = { workspace = true }
117119
data-encoding = { workspace = true }
118120
anyhow = { workspace = true, features = ["backtrace"] }
@@ -264,7 +266,7 @@ z85 = { workspace = true, optional = true }
264266
etl = { workspace = true, optional = true }
265267

266268
[package.metadata.cargo-machete]
267-
ignored = ["aws-sigv4", "deltalake-catalog-unity"]
269+
ignored = ["aws-sigv4", "deltalake-catalog-unity", "tokio-websockets"]
268270

269271
[target.'cfg(target_os = "linux")'.dependencies]
270272
# Optional so the SQL test fixture (sql-to-dbsp-compiler/temp), which builds
Collapse file

‎crates/connectors/src/transport/nats/input.rs‎

Copy file name to clipboardExpand all lines: crates/connectors/src/transport/nats/input.rs
+76-18Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ impl NatsReader {
501501

502502
// A replay consumer is single-use: whether the replay
503503
// succeeded or not, the next attempt creates a fresh one.
504-
delete_nats_consumer(&jetstream, &stream_ctx, &consumer_name).await;
504+
delete_consumer_family(&jetstream, &stream_ctx, &consumer_name).await;
505505

506506
result.map_err(|error| {
507507
error.with_context(format!(
@@ -929,9 +929,9 @@ impl NatsReader {
929929
/// Every consumer gets this prefix plus a unique suffix (see
930930
/// [`create_nats_consumer`]), so consumers are always named by us rather than
931931
/// by the server. Server-assigned names are opaque, which makes it impossible
932-
/// to attribute a consumer to a pipeline from the NATS side and, worse, keeps
933-
/// the ordered consumer's self-recreation from ever deleting the consumer it
934-
/// replaces (it deletes by the client-side name, which then never matches).
932+
/// to attribute a consumer to a pipeline from the NATS side. The client
933+
/// library names its own recreations of a consumer after it, see
934+
/// [`list_consumer_family`].
935935
fn consumer_name_prefix(consumer_config: &cfg::ConsumerConfig, table_name: &str) -> String {
936936
let base = consumer_config
937937
.name
@@ -1096,6 +1096,71 @@ async fn delete_nats_consumer(
10961096
}
10971097
}
10981098

1099+
/// Names the consumers on the stream that descend from `consumer_name`: the
1100+
/// consumer itself and the `{consumer_name}_{n}` ones the client library
1101+
/// creates when it recreates an ordered consumer after a gap, a lost
1102+
/// heartbeat or an external deletion. The library deletes the consumer it
1103+
/// replaces, so at most one of them normally exists. The unique suffix of
1104+
/// our own names (see [`create_nats_consumer`]) keeps one connector's
1105+
/// consumers out of another's family.
1106+
async fn list_consumer_family(
1107+
jetstream: &jetstream::Context,
1108+
stream_name: &str,
1109+
consumer_name: &str,
1110+
) -> AnyResult<Vec<String>> {
1111+
use futures::TryStreamExt;
1112+
let stream = jetstream.get_stream_no_info(stream_name).await?;
1113+
let recreated_prefix = format!("{consumer_name}_");
1114+
let mut names = stream.consumer_names();
1115+
let mut family = Vec::new();
1116+
while let Some(name) = names.try_next().await? {
1117+
if name == consumer_name || name.starts_with(&recreated_prefix) {
1118+
family.push(name);
1119+
}
1120+
}
1121+
Ok(family)
1122+
}
1123+
1124+
/// Deletes the consumer and every recreation of it, see
1125+
/// [`list_consumer_family`]. Falls back to deleting `consumer_name` alone
1126+
/// when the listing fails.
1127+
async fn delete_consumer_family(
1128+
jetstream: &jetstream::Context,
1129+
stream_ctx: &StreamContext,
1130+
consumer_name: &str,
1131+
) {
1132+
let stream_name = stream_ctx.stream_name.as_str();
1133+
match tokio::time::timeout(
1134+
CONSUMER_DELETE_TIMEOUT,
1135+
list_consumer_family(jetstream, stream_name, consumer_name),
1136+
)
1137+
.await
1138+
{
1139+
Ok(Ok(family)) => {
1140+
if family.is_empty() {
1141+
debug!(
1142+
"No consumer named '{consumer_name}' or recreated from it is left on stream '{stream_name}'"
1143+
);
1144+
}
1145+
for name in family {
1146+
delete_nats_consumer(jetstream, stream_ctx, &name).await;
1147+
}
1148+
}
1149+
Ok(Err(error)) => {
1150+
warn!(
1151+
"Failed to list the consumers of stream '{stream_name}', deleting only '{consumer_name}': {error:#}"
1152+
);
1153+
delete_nats_consumer(jetstream, stream_ctx, consumer_name).await;
1154+
}
1155+
Err(_) => {
1156+
warn!(
1157+
"Timed out after {CONSUMER_DELETE_TIMEOUT:?} listing the consumers of stream '{stream_name}', deleting only '{consumer_name}'"
1158+
);
1159+
delete_nats_consumer(jetstream, stream_ctx, consumer_name).await;
1160+
}
1161+
}
1162+
}
1163+
10991164
/// Grace period before re-checking whether a consumer we just deleted has
11001165
/// reappeared.
11011166
const DELAYED_CREATE_RECHECK_DELAY: Duration = Duration::from_secs(2);
@@ -1108,7 +1173,8 @@ const DELAYED_CREATE_RECHECK_DELAY: Duration = Duration::from_secs(2);
11081173
/// apply the create after the delete, leaving an orphan that nobody will
11091174
/// ever read and that scans the stream on the server's behalf. A delete
11101175
/// sent immediately after would race the same reordering, so this instead
1111-
/// waits out a grace period, then deletes the consumer again if it exists.
1176+
/// waits out a grace period, then deletes the consumer and any recreation
1177+
/// of it (see [`list_consumer_family`]) that exists by then.
11121178
///
11131179
/// Spawned rather than awaited: the caller (a create path, or a pause
11141180
/// tearing down a reader) should not block on this best-effort cleanup.
@@ -1122,16 +1188,7 @@ fn clean_up_delayed_create(
11221188
let consumer_name = consumer_name.to_string();
11231189
tokio::spawn(async move {
11241190
tokio::time::sleep(DELAYED_CREATE_RECHECK_DELAY).await;
1125-
let reappeared = jetstream
1126-
.get_consumer_from_stream::<nats_consumer::Config, _, _>(
1127-
&consumer_name,
1128-
&stream_ctx.stream_name,
1129-
)
1130-
.await
1131-
.is_ok();
1132-
if reappeared {
1133-
delete_nats_consumer(&jetstream, &stream_ctx, &consumer_name).await;
1134-
}
1191+
delete_consumer_family(&jetstream, &stream_ctx, &consumer_name).await;
11351192
});
11361193
}
11371194

@@ -1526,16 +1583,17 @@ impl Canceller {
15261583
// The reader task is gone, but the client library may have already
15271584
// sent its own recreate of this consumer (on a detected gap or lost
15281585
// heartbeat) before cancellation reached it. That recreate can still
1529-
// land after our delete below, the same reordering
1530-
// `clean_up_delayed_create` exists for, so check for it too.
1586+
// land after the cleanup below has looked for it, the same
1587+
// reordering `clean_up_delayed_create` exists for, so check for it
1588+
// too.
15311589
let Canceller {
15321590
jetstream,
15331591
stream_ctx,
15341592
consumer_name,
15351593
..
15361594
} = self;
15371595
async move {
1538-
delete_nats_consumer(&jetstream, &stream_ctx, &consumer_name).await;
1596+
delete_consumer_family(&jetstream, &stream_ctx, &consumer_name).await;
15391597
clean_up_delayed_create(&jetstream, &stream_ctx, &consumer_name);
15401598
}
15411599
}
Collapse file

‎crates/connectors/src/transport/nats/input/test/hardening_tests.rs‎

Copy file name to clipboardExpand all lines: crates/connectors/src/transport/nats/input/test/hardening_tests.rs
+36Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,42 @@ pub fn deleted_positioned_consumer_does_not_reingest(engine: &dyn ConnectorTestE
512512
.unwrap();
513513
}
514514

515+
/// A consumer the client library recreated must not outlive a pause.
516+
///
517+
/// The library names a recreated ordered consumer `{name}_{n}` and deletes
518+
/// the one it replaces, so the connector must delete the recreated consumer
519+
/// rather than the name it chose itself.
520+
#[engine_test(services(Nats))]
521+
pub fn library_recreated_consumer_is_deleted_on_pause(engine: &dyn ConnectorTestEngine) {
522+
use NatsMockAction::*;
523+
run_nats_mock_test(
524+
engine,
525+
basic_nats_config,
526+
&[
527+
StartNats,
528+
CreateStream,
529+
Publish(2),
530+
CreatePipeline,
531+
Extend,
532+
WaitForRecords(2),
533+
DeleteConsumers,
534+
// The library notices the deletion and recreates the consumer.
535+
WaitForConsumerCount {
536+
count: 1,
537+
timeout: CONSUMER_COUNT_TIMEOUT,
538+
},
539+
Pause,
540+
WaitForConsumerCount {
541+
count: 0,
542+
timeout: CONSUMER_COUNT_TIMEOUT,
543+
},
544+
AssertConsumerCountersMatchServer,
545+
Disconnect,
546+
],
547+
)
548+
.unwrap();
549+
}
550+
515551
/// The consumer counters must agree with the server when a create times out
516552
/// but the server created the consumer anyway.
517553
///
Collapse file

‎crates/connectors/src/transport/nats/input/test/jetstream_proxy.rs‎

Copy file name to clipboardExpand all lines: crates/connectors/src/transport/nats/input/test/jetstream_proxy.rs
+11-2Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ pub struct ProxyFaults {
3636
/// Drops the server's reply to the nth consumer create. The server still
3737
/// creates the consumer.
3838
pub drop_create_reply: Option<usize>,
39-
/// Holds the nth consumer create until a delete of the same consumer has
40-
/// reached the server, then forwards the create.
39+
/// Holds the nth consumer create until a delete of the same consumer, or
40+
/// a listing of the stream's consumers, has reached the server, then
41+
/// forwards the create.
4142
pub hold_create_until_delete: Option<usize>,
4243
/// Holds every consumer delete for this long before forwarding it.
4344
pub delay_deletes: Option<Duration>,
@@ -336,6 +337,14 @@ async fn proxy_connection(
336337
state.dropped_replies.insert(reply.to_string());
337338
}
338339
to_server.push(frame.bytes());
340+
// The connector lists the stream's consumers before
341+
// deleting; a held create landing right after that
342+
// listing is the reordering the hold stands for.
343+
if subject.starts_with("$JS.API.CONSUMER.NAMES.")
344+
&& let Some((_, create)) = state.held_create.take()
345+
{
346+
to_server.push(create);
347+
}
339348
}
340349
}
341350
if let Some(gate) = request_gate {
Collapse file

‎crates/connectors/src/transport/nats/output.rs‎

Copy file name to clipboardExpand all lines: crates/connectors/src/transport/nats/output.rs
+7-6Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,17 @@ enum PublishFailure {
125125

126126
/// Classifies an ack error from `async-nats`.
127127
///
128-
/// `Other` carries server errors the client does not map to a kind (such as
129-
/// an exceeded message size, which the pre-publish check keeps off this
130-
/// path): retried, because a transient server-side condition also lands
131-
/// here, and the retry loop logs the error text so a permanent one is
132-
/// visible in the logs.
128+
/// `MaxPayloadExceeded` is the client's own size check, which the
129+
/// pre-publish check keeps off this path. `Other` carries server errors the
130+
/// client does not map to a kind: retried, because a transient server-side
131+
/// condition also lands here, and the retry loop logs the error text so a
132+
/// permanent one is visible in the logs.
133133
fn classify(error: jetstream::context::PublishError) -> PublishFailure {
134134
match error.kind() {
135135
PublishErrorKind::StreamNotFound
136136
| PublishErrorKind::WrongLastMessageId
137-
| PublishErrorKind::WrongLastSequence => PublishFailure::Permanent(error.into()),
137+
| PublishErrorKind::WrongLastSequence
138+
| PublishErrorKind::MaxPayloadExceeded => PublishFailure::Permanent(error.into()),
138139
PublishErrorKind::TimedOut
139140
| PublishErrorKind::BrokenPipe
140141
| PublishErrorKind::MaxAckPending
Collapse file

‎docs.feldera.com/docs/changelog.md‎

Copy file name to clipboardExpand all lines: docs.feldera.com/docs/changelog.md
+8Lines changed: 8 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ Source edition can be found on github.
2626
counts, and queue wait time. See
2727
[Kafka output delivery and metrics](/connectors/sinks/kafka#delivery-retries-and-backpressure).
2828

29+
- NATS input connector: the client library is updated to async-nats 0.51.0,
30+
which fixes spurious missing-heartbeat errors while messages are buffered
31+
and pull consumers looping on `409 Exceeded MaxRequest*`. The `max_batch`,
32+
`max_bytes` and `max_expiry` consumer settings now reach the server, where
33+
earlier releases dropped them. A consumer that the library recreates is
34+
named `<prefix>_<uuid>_<n>`, and the connector deletes it together with its
35+
own. See [consumer lifecycle](/connectors/sources/nats#consumer-lifecycle).
36+
2937
## v0.371.0
3038

3139
- Delta Lake input connector: `uc://` tables can use Databricks workload
Collapse file

‎docs.feldera.com/docs/connectors/sources/nats.md‎

Copy file name to clipboardExpand all lines: docs.feldera.com/docs/connectors/sources/nats.md
+9-5Lines changed: 9 additions & 5 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ The connector configuration consists of three main sections:
5050
| `deliver_policy` | variant | Yes | Starting point for reading from the stream (see [Deliver Policy](#deliver-policy)) |
5151
| `max_waiting` | integer | No | Maximum outstanding pull requests. Default: 0 |
5252
| `metadata` | map (string → string) | No | Consumer metadata key-value pairs |
53-
| `max_batch` | integer | No | Maximum messages per batch |
54-
| `max_bytes` | integer | No | Maximum bytes per batch |
53+
| `max_batch` | integer | No | Maximum messages per pull request. The server rejects larger pull requests, and the connector sizes its own accordingly |
54+
| `max_bytes` | integer | No | Maximum bytes per pull request |
5555
| `max_expiry` | duration | No | Maximum time a pull request stays parked on the server, for example `30s`. Replaces the deprecated `max_expires`. |
5656

5757
#### Deliver Policy
@@ -145,14 +145,18 @@ or the table name if none is set. Characters that JetStream does not allow in
145145
consumer names are replaced with `_`. The unique suffix prevents "consumer
146146
already exists" errors on quick restarts, and the prefix identifies which
147147
connector owns a consumer when you inspect the stream with `nats consumer ls`.
148-
For example, to list the consumers of a connector whose prefix is `orders`:
148+
When the client library recreates a consumer after a gap, a lost heartbeat, or
149+
an external deletion, the replacement is named `<prefix>_<uuid>_<n>` and the
150+
replaced consumer is deleted. For example, to list the consumers of a
151+
connector whose prefix is `orders`:
149152

150153
```bash
151154
nats consumer ls <stream> --names | grep '^orders_'
152155
```
153156

154-
The connector deletes its consumer when it pauses, stops, abandons a failed
155-
reader before retrying, or finishes a replay. It also deletes the consumer
157+
The connector deletes its consumer, and any replacement the client library
158+
made for it, when it pauses, stops, abandons a failed reader before retrying,
159+
or finishes a replay. It also deletes the consumer
156160
after a create request that timed out, because an overloaded server may still
157161
create the consumer after the client has given up. Deletion is best-effort: if
158162
the server cannot be reached, the consumer expires on its own after 30 seconds

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.