transport: complete M0–M7
Some checks failed
ci / rust (push) Failing after 2m21s
ci / ui (push) Failing after 28s
images / build-and-push (push) Failing after 18s

shared: add stream+consumer policy helpers; NATS context header builder

aggregate/runner/projection: centralize stream validation and header usage; set bounded consumer params

projection: add QueryService gRPC and wire into main; settings include PROJECTION_GRPC_ADDR

gateway: gRPC routing to Projection/Runner with deadlines; bounded read-only retries; pooled gRPC channels (bounded LRU+TTL); admin proxy forwards to gRPC; probes use concurrency limiter + TTL cache

runner: add RunnerAdmin gRPC server (drain, status, reload) and wire into main; settings include RUNNER_GRPC_ADDR

tests: add gateway authz for runner admin, projection tenant isolation, runner admin drain semantics

docs: update TRANSPORT_DEVELOPMENT_PLAN to reflect completed milestones and details
This commit is contained in:
2026-03-30 14:24:14 +03:00
parent 1ab112438b
commit 90c307016d
41 changed files with 2391 additions and 505 deletions

View File

@@ -2,7 +2,7 @@ use crate::config::Settings;
use crate::types::ProjectionError;
use async_nats::jetstream::{
self, consumer::pull::Config as PullConfig, consumer::AckPolicy, consumer::DeliverPolicy,
consumer::ReplayPolicy,
consumer::ReplayPolicy, stream::Config as StreamConfig,
};
#[derive(Debug, Clone)]
@@ -24,7 +24,7 @@ impl JetStreamClient {
.subject_filters
.first()
.cloned()
.unwrap_or_else(|| "tenant.*.aggregate.*.*".to_string());
.unwrap_or_else(|| shared::NATS_SUBJECT_AGGREGATE_EVENTS_ALL.to_string());
let options = ConsumerOptions {
durable_name: settings.durable_name.clone(),
@@ -45,20 +45,32 @@ impl JetStreamClient {
let jetstream = jetstream::new(client);
let stream = jetstream
.get_stream(&settings.stream_name)
let expected = stream_policy_config(&settings.stream_name);
let mut stream = jetstream
.get_or_create_stream(expected.clone())
.await
.map_err(|e| ProjectionError::StreamError(format!("Stream not found: {}", e)))?;
.map_err(|e| ProjectionError::StreamError(format!("Stream error: {}", e)))?;
let info = stream
.info()
.await
.map_err(|e| ProjectionError::StreamError(format!("Stream info error: {}", e)))?;
validate_stream_config(&expected, &info.config)?;
let policy = shared::consumer_policy_from_parts(
settings.ack_timeout_ms,
settings.max_in_flight,
settings.max_deliver,
);
let consumer_config = PullConfig {
durable_name: Some(options.durable_name.clone()),
deliver_policy: options.deliver_policy,
ack_policy: AckPolicy::Explicit,
ack_wait: std::time::Duration::from_millis(settings.ack_timeout_ms),
ack_wait: policy.ack_wait,
filter_subject: options.filter_subject,
replay_policy: ReplayPolicy::Instant,
max_ack_pending: settings.max_in_flight as i64,
max_deliver: settings.max_deliver,
max_ack_pending: policy.max_ack_pending,
max_deliver: policy.max_deliver,
..Default::default()
};
@@ -88,3 +100,43 @@ impl JetStreamClient {
Ok(info.state.last_sequence)
}
}
fn stream_policy_config(name: &str) -> StreamConfig {
let policy = shared::stream_policy_defaults(
name.to_string(),
vec![shared::NATS_SUBJECT_AGGREGATE_EVENTS_ALL.to_string()],
);
StreamConfig {
name: policy.name,
subjects: policy.subjects,
max_messages: policy.max_messages,
max_bytes: policy.max_bytes,
max_age: policy.max_age,
duplicate_window: policy.duplicate_window,
..Default::default()
}
}
fn validate_stream_config(
expected: &StreamConfig,
actual: &StreamConfig,
) -> Result<(), ProjectionError> {
let expected = shared::stream_policy_from_parts(
expected.name.as_str(),
expected.subjects.clone(),
expected.max_messages,
expected.max_bytes,
expected.max_age,
expected.duplicate_window,
);
let actual = shared::stream_policy_from_parts(
actual.name.as_str(),
actual.subjects.clone(),
actual.max_messages,
actual.max_bytes,
actual.max_age,
actual.duplicate_window,
);
shared::validate_stream_policy(&expected, &actual)
.map_err(|e| ProjectionError::StreamError(e.to_string()))
}