use crate::error::{LaserError, decode_managed_reply};
use crate::filters::reader::FilteredReaderBuilder;
use crate::laser::Laser;
use laser_wire::codes::{
AGDX_FILTER_MUTATE_CODE, AGDX_FILTER_OPERATION_CODE, AGDX_FILTER_PREVIEW_CODE,
AGDX_FILTER_TEST_CODE, AGDX_FILTER_VALIDATE_CODE, AGDX_GET_FILTER_BINDING_CODE,
AGDX_GET_FILTER_CODE, AGDX_LIST_FILTER_BINDINGS_CODE, AGDX_LIST_FILTER_REVISIONS_CODE,
AGDX_LIST_FILTERS_CODE, FILTER_OP_VERSION,
};
use laser_wire::filter::{
ConsumerFilter, FilterBinding, FilterBindingPage, FilterCatalogOutcome, FilterCatalogReply,
FilterDetail, FilterError, FilterErrorReason, FilterGroupRef, FilterHeader, FilterMutation,
FilterMutationOutcome, FilterMutationRequest, FilterMutationResult, FilterMutationStatus,
FilterOutcome, FilterPage, FilterPreview, FilterPreviewRequest, FilterRef, FilterReply,
FilterRevisionPage, FilterRevisionRef, FilterSource, FilterState, FilterTestRequest,
FilterTestResult, FilterValidation, GetFilter, GetFilterBinding, GetFilterOperation,
ListFilterBindings, ListFilterRevisions, ListFilters,
};
use laser_wire::framing::encode_named;
use laser_wire::limits::{MAX_FILTER_PREVIEW_EXAMINED, MAX_FILTER_PREVIEW_RECORDS};
use laser_wire::schema::Digest32;
use laser_wire::validate::Validate;
use serde::Serialize;
use std::time::Duration;
use tokio::time::{Instant, sleep};
const DEFAULT_PAGE_SIZE: u32 = 50;
const DEFAULT_PREVIEW_RECORDS: u32 = 20;
const MUTATION_ATTEMPTS: u32 = 3;
const MUTATION_RETRY_BACKOFF: Duration = Duration::from_millis(200);
const OUTCOME_POLL_INTERVAL: Duration = Duration::from_millis(100);
pub const DEFAULT_OUTCOME_WAIT: Duration = Duration::from_secs(30);
impl Laser {
pub fn filters(&self) -> Filters<'_> {
Filters { laser: self }
}
}
pub struct Filters<'a> {
laser: &'a Laser,
}
impl<'a> Filters<'a> {
pub fn reader(
&self,
stream: impl Into<String>,
topic: impl Into<String>,
) -> FilteredReaderBuilder<'a> {
FilteredReaderBuilder::new(
self.laser,
FilterSource {
stream: stream.into(),
topic: topic.into(),
},
)
}
pub async fn validate(&self, filter: &ConsumerFilter) -> Result<FilterValidation, LaserError> {
filter.validate()?;
match self.native(AGDX_FILTER_VALIDATE_CODE, filter).await? {
FilterOutcome::Validated(validation) => Ok(validation),
_ => Err(unexpected("validate")),
}
}
pub async fn test(
&self,
filter: FilterRef,
payload: impl Into<Vec<u8>>,
headers: Vec<FilterHeader>,
) -> Result<FilterTestResult, LaserError> {
let request = FilterTestRequest {
v: FILTER_OP_VERSION,
filter,
payload: payload.into(),
headers,
};
request.validate()?;
match self.native(AGDX_FILTER_TEST_CODE, &request).await? {
FilterOutcome::Tested(result) => Ok(result),
_ => Err(unexpected("test")),
}
}
pub fn preview(
&self,
stream: impl Into<String>,
topic: impl Into<String>,
partition_id: u32,
filter: FilterRef,
) -> FilterPreviewBuilder<'a> {
FilterPreviewBuilder {
filters: Filters { laser: self.laser },
request: FilterPreviewRequest {
v: FILTER_OP_VERSION,
source: FilterSource {
stream: stream.into(),
topic: topic.into(),
},
partition_id,
filter,
from_offset: 0,
max_examined: MAX_FILTER_PREVIEW_EXAMINED,
max_records: DEFAULT_PREVIEW_RECORDS.min(MAX_FILTER_PREVIEW_RECORDS),
explain: false,
},
}
}
pub async fn get(&self, filter_id: u32) -> Result<FilterDetail, LaserError> {
let request = GetFilter {
v: FILTER_OP_VERSION,
filter_id,
};
match self.catalog(AGDX_GET_FILTER_CODE, &request).await? {
FilterCatalogOutcome::Filter(detail) => Ok(detail),
_ => Err(unexpected("get")),
}
}
pub fn list(&self) -> FilterList<'a> {
FilterList {
filters: Filters { laser: self.laser },
request: ListFilters {
v: FILTER_OP_VERSION,
name_contains: None,
state: None,
before_id: None,
page: 0,
page_size: DEFAULT_PAGE_SIZE,
},
}
}
pub async fn revisions(
&self,
filter_id: u32,
page: u32,
page_size: u32,
) -> Result<FilterRevisionPage, LaserError> {
let request = ListFilterRevisions {
v: FILTER_OP_VERSION,
filter_id,
page,
page_size,
};
request.validate()?;
match self
.catalog(AGDX_LIST_FILTER_REVISIONS_CODE, &request)
.await?
{
FilterCatalogOutcome::Revisions(revisions) => Ok(revisions),
_ => Err(unexpected("revisions")),
}
}
pub async fn binding(&self, group: FilterGroupRef) -> Result<FilterBinding, LaserError> {
let request = GetFilterBinding {
v: FILTER_OP_VERSION,
group,
identity: None,
};
request.validate()?;
match self.catalog(AGDX_GET_FILTER_BINDING_CODE, &request).await? {
FilterCatalogOutcome::Binding(binding) => Ok(binding),
_ => Err(unexpected("binding")),
}
}
pub async fn bindings(
&self,
filter_id: Option<u32>,
stream: Option<&str>,
topic: Option<&str>,
page: u32,
page_size: u32,
) -> Result<FilterBindingPage, LaserError> {
let request = ListFilterBindings {
v: FILTER_OP_VERSION,
filter_id,
stream: stream.map(str::to_owned),
topic: topic.map(str::to_owned),
page,
page_size,
};
request.validate()?;
match self
.catalog(AGDX_LIST_FILTER_BINDINGS_CODE, &request)
.await?
{
FilterCatalogOutcome::Bindings(bindings) => Ok(bindings),
_ => Err(unexpected("bindings")),
}
}
pub async fn operation(&self, operation_id: u128) -> Result<FilterMutationOutcome, LaserError> {
let request = GetFilterOperation {
v: FILTER_OP_VERSION,
operation_id,
};
match self.catalog(AGDX_FILTER_OPERATION_CODE, &request).await? {
FilterCatalogOutcome::Mutation(outcome) => Ok(outcome),
_ => Err(unexpected("operation")),
}
}
pub async fn register(
&self,
name: impl Into<String>,
filter: ConsumerFilter,
description: impl Into<String>,
) -> Result<FilterRevisionRef, LaserError> {
let mutation = FilterMutation::Register {
name: name.into(),
description: description.into(),
filter,
};
match self.apply(mutation).await? {
FilterMutationResult::Registered(revision) => Ok(revision),
_ => Err(unexpected("register")),
}
}
pub async fn revise(
&self,
filter_id: u32,
expected_revision: u32,
filter: ConsumerFilter,
) -> Result<FilterRevisionRef, LaserError> {
let mutation = FilterMutation::Revise {
filter_id,
expected_revision,
filter,
};
match self.apply(mutation).await? {
FilterMutationResult::Revised(revision) => Ok(revision),
_ => Err(unexpected("revise")),
}
}
pub async fn describe(
&self,
filter_id: u32,
description: impl Into<String>,
) -> Result<(), LaserError> {
let mutation = FilterMutation::Describe {
filter_id,
description: description.into(),
};
self.apply(mutation).await.map(|_| ())
}
pub async fn archive(&self, filter_id: u32) -> Result<(), LaserError> {
self.apply(FilterMutation::Archive { filter_id })
.await
.map(|_| ())
}
pub async fn delete(&self, filter_id: u32) -> Result<(), LaserError> {
self.apply(FilterMutation::Drop { filter_id })
.await
.map(|_| ())
}
pub async fn bind(
&self,
group: FilterGroupRef,
filter_id: u32,
revision: u32,
) -> Result<FilterBinding, LaserError> {
let mutation = FilterMutation::Bind {
group,
filter_id,
revision,
};
match self.apply(mutation).await? {
FilterMutationResult::Bound(binding) => Ok(binding),
_ => Err(unexpected("bind")),
}
}
pub async fn create_consumer_group(
&self,
group: FilterGroupRef,
filter_id: u32,
revision: u32,
) -> Result<FilterBinding, LaserError> {
self.catalog_capabilities().await?;
self.laser
.stream(&group.stream)
.topic(&group.topic)
.ensure_consumer_group(&group.group)
.await?;
self.bind(group, filter_id, revision).await
}
pub async fn set_revision_enabled(
&self,
filter_id: u32,
revision: u32,
enabled: bool,
) -> Result<(), LaserError> {
self.apply(FilterMutation::SetRevisionEnabled {
filter_id,
revision,
enabled,
})
.await
.map(|_| ())
}
pub async fn unbind(
&self,
group: FilterGroupRef,
expected_digest: Digest32,
) -> Result<FilterBinding, LaserError> {
let mutation = FilterMutation::Unbind {
group,
expected_digest,
expected_identity: None,
};
match self.apply(mutation).await? {
FilterMutationResult::Unbound(binding) => Ok(binding),
_ => Err(unexpected("unbind")),
}
}
pub async fn unbind_binding(
&self,
binding: FilterBinding,
) -> Result<FilterBinding, LaserError> {
match self
.apply(FilterMutation::Unbind {
group: binding.group,
expected_digest: binding.digest,
expected_identity: Some(binding.identity),
})
.await?
{
FilterMutationResult::Unbound(binding) => Ok(binding),
_ => Err(unexpected("unbind")),
}
}
pub async fn apply(
&self,
mutation: FilterMutation,
) -> Result<FilterMutationResult, LaserError> {
self.apply_as(u128::from(ulid::Ulid::generate()), mutation)
.await
}
pub async fn apply_as(
&self,
operation_id: u128,
mutation: FilterMutation,
) -> Result<FilterMutationResult, LaserError> {
let outcome = self.submit(operation_id, mutation).await?;
let status = match outcome.status {
FilterMutationStatus::Pending => {
return self
.wait_for_outcome(operation_id, DEFAULT_OUTCOME_WAIT)
.await;
}
status => status,
};
settled(operation_id, status)
}
pub async fn mutate(
&self,
operation_id: u128,
mutation: FilterMutation,
) -> Result<FilterMutationOutcome, LaserError> {
let request = FilterMutationRequest {
v: FILTER_OP_VERSION,
operation_id,
mutation,
};
request.validate()?;
match self.catalog(AGDX_FILTER_MUTATE_CODE, &request).await? {
FilterCatalogOutcome::Mutation(outcome) => Ok(outcome),
_ => Err(unexpected("mutate")),
}
}
pub async fn wait_for_outcome(
&self,
operation_id: u128,
timeout: Duration,
) -> Result<FilterMutationResult, LaserError> {
let deadline = Instant::now() + timeout;
loop {
match self.operation(operation_id).await {
Ok(FilterMutationOutcome {
status: FilterMutationStatus::Pending,
..
}) => {}
Ok(outcome) => return settled(operation_id, outcome.status),
Err(error) if error.is_not_found() || error.is_retryable() => {}
Err(error) => return Err(error),
}
if Instant::now() >= deadline {
return Err(LaserError::AmbiguousMutation(format!(
"filter mutation {operation_id} has no outcome yet"
)));
}
sleep(OUTCOME_POLL_INTERVAL).await;
}
}
async fn submit(
&self,
operation_id: u128,
mutation: FilterMutation,
) -> Result<FilterMutationOutcome, LaserError> {
let mut attempt = 1;
loop {
match self.mutate(operation_id, mutation.clone()).await {
Err(error) if error.is_retryable() && attempt < MUTATION_ATTEMPTS => {
attempt += 1;
sleep(MUTATION_RETRY_BACKOFF).await;
}
Err(error) if error.is_retryable() => {
return Err(LaserError::AmbiguousMutation(format!(
"filter mutation {operation_id}: {error}"
)));
}
result => return result,
}
}
}
pub(crate) async fn native(
&self,
code: u32,
request: &impl Serialize,
) -> Result<FilterOutcome, LaserError> {
if !self.laser.capabilities().await.filters.native {
return Err(LaserError::unsupported(
"filters",
"consumer filters are not served by this server",
));
}
let payload = encode_named(request)
.map_err(|error| LaserError::Codec(format!("encode filter request: {error}")))?;
let reply = self.laser.send_raw_with_response(code, payload).await?;
match decode_managed_reply::<FilterReply>(&reply)? {
FilterReply::Ok(outcome) => Ok(outcome),
FilterReply::Err(error) => Err(error.into()),
}
}
async fn catalog_capabilities(&self) -> Result<(), LaserError> {
let capabilities = self.laser.capabilities().await;
if !capabilities.filters.native || !capabilities.filters.catalog {
return Err(LaserError::unsupported_feature(
"filters",
"catalog",
"the saved-filter catalog is not served by this deployment",
));
}
if let Some(versions) = capabilities.versions
&& versions.filter != FILTER_OP_VERSION
{
return Err(FilterError::new(
FilterErrorReason::VersionSkew,
format!(
"the catalog serves filter op version {}, this SDK speaks {FILTER_OP_VERSION}",
versions.filter
),
)
.into());
}
Ok(())
}
async fn catalog(
&self,
code: u32,
request: &impl Serialize,
) -> Result<FilterCatalogOutcome, LaserError> {
self.catalog_capabilities().await?;
let payload = encode_named(request)
.map_err(|error| LaserError::Codec(format!("encode filter request: {error}")))?;
let reply = self.laser.send_raw_with_response(code, payload).await?;
match decode_managed_reply::<FilterCatalogReply>(&reply)? {
FilterCatalogReply::Ok(outcome) => Ok(*outcome),
FilterCatalogReply::Err(error) => Err(error.into()),
}
}
}
pub struct FilterPreviewBuilder<'a> {
filters: Filters<'a>,
request: FilterPreviewRequest,
}
impl FilterPreviewBuilder<'_> {
#[must_use]
pub fn from_offset(mut self, offset: u64) -> Self {
self.request.from_offset = offset;
self
}
#[must_use]
pub fn max_examined(mut self, count: u32) -> Self {
self.request.max_examined = count;
self
}
#[must_use]
pub fn max_records(mut self, count: u32) -> Self {
self.request.max_records = count;
self
}
#[must_use]
pub fn explain(mut self, explain: bool) -> Self {
self.request.explain = explain;
self
}
pub async fn send(self) -> Result<FilterPreview, LaserError> {
self.request.validate()?;
match self
.filters
.native(AGDX_FILTER_PREVIEW_CODE, &self.request)
.await?
{
FilterOutcome::Preview(preview) => Ok(preview),
_ => Err(unexpected("preview")),
}
}
}
pub struct FilterList<'a> {
filters: Filters<'a>,
request: ListFilters,
}
impl FilterList<'_> {
#[must_use]
pub fn name_contains(mut self, text: impl Into<String>) -> Self {
self.request.name_contains = Some(text.into());
self
}
#[must_use]
pub fn state(mut self, state: FilterState) -> Self {
self.request.state = Some(state);
self
}
#[must_use]
pub const fn before(mut self, filter_id: u32) -> Self {
self.request.before_id = Some(filter_id);
self
}
#[must_use]
pub fn page(mut self, page: u32, page_size: u32) -> Self {
self.request.page = page;
self.request.page_size = page_size;
self
}
pub async fn send(self) -> Result<FilterPage, LaserError> {
self.request.validate()?;
match self
.filters
.catalog(AGDX_LIST_FILTERS_CODE, &self.request)
.await?
{
FilterCatalogOutcome::Filters(page) => Ok(page),
_ => Err(unexpected("list")),
}
}
}
fn settled(
operation_id: u128,
status: FilterMutationStatus,
) -> Result<FilterMutationResult, LaserError> {
match status {
FilterMutationStatus::Applied(result) => Ok(result),
FilterMutationStatus::Rejected(error) => Err(error.into()),
FilterMutationStatus::Pending => Err(LaserError::AmbiguousMutation(format!(
"filter mutation {operation_id} has no outcome yet"
))),
}
}
fn unexpected(verb: &str) -> LaserError {
LaserError::Protocol(format!("filters {verb}: unexpected reply variant"))
}