Repository navigation
Releases: jamescarr/ankusa
Release list
sdk-java-v0.1.0
Added
-
AdminClient.listAudit(ListAuditParams):GET /v1/audit, the audit trail
(newest first, paged bycursor/limit, filtered bysince,until,
actor,action,tenant). -
revisiononRoute: counts the definition's changes, from 1 at creation;
re-creating a deleted route starts again at 1; null from a node older than
the field. -
scopeonAdminRejectedErrorandRoutesRejectedError: the scope a403 forbiddensays the credential lacks (control planes require a credential
since Ankusa 0.6). -
verificationon parsed headers, decoded messages and received deliveries:
how Ankusa verified the hook at ingest ({status, flagged},nullfrom a
node older than the field). -
ParsedClaimRef.parseandClaimCheckClient.redeemaccept signed,
expiring v2 refs
(urn:ankusa:claim:v2:<tenant>:<claim_id>:<exp_unix>:<signature>): the
parsedpathends in?exp=&sig=andredeemrequests it as is. The
gateway's400 bad_signatureand410 ref_expiredare a
ClaimRejectedError. The SDK never checks the signature or the expiry. -
webhook/Receiver.receive(headers, body, Receiver.Options), the
receiver-side decision on one HTTP-sink delivery. The sink signs every
delivery unless configuredallow_unsigned: true, so a receiver is closed
by default:Options.signed(secrets)makes the delivery verify (as
Signature.verify);Options.unsigned()is the explicit opt-out, and any
other options with no secrets throwInvalidSignatureError
(invalid_secret). The signature is judged before the hook id. Returns a
Receiver.Received(headers,verified). -
ClaimCheckClient:redeem(ref, sha256)fetches a claim's bytes from the
claim-check gateway and verifies them against the expected digest before
returning them, andhealth()is a liveness probe.ParsedClaimRef.parse
(classclaimcheck/ParsedClaimRef.java) exposes the ref parsing on its own. -
RoutesClient: the route-management client —health,listRoutes,
createRoute,getRoute,replaceRoute,updateRoute,deleteRoute,
getIpRules,putIpRules, andtestRoute. Every call is one request, no
retries, and a redirect is never followed: a3xxis aRoutesUnavailableError. -
AdminClient: the DLQ and quarantine client —health,metrics,config,
asyncApi(theGET /asyncapi.jsonAsyncAPI 3.0 document),
listDeadLetters,listQuarantined,purgeQuarantined
(DELETE /v1/quarantine, filtered byPurgeQuarantinedParams, answering a
QuarantinePurgeofdeletedandbytes), and the replay jobs
createReplay,getReplay,listReplays, andupdateReplay(kindsdlq,
archive,quarantine). AQuarantineEntrycarries a nullabletenantId
andsize, both null for an entry held before the listener recorded them. -
Message.decode, the queue-message decoder: it validates the v1 message
(including the shippedidempotency_key, theidempotencyKeyrecord
component), decodes and verifies the body, and raisesInvalidMessageError
(code(),field()) on a bad message.Message.idempotencyKey(includeReplay)
andHookHeaders.idempotencyKey(includeReplay)return the key a worker dedupes
on: the key Ankusa shipped, or for a message or delivery that predates it
tenant:source_id:dedupe_key(tenantdefaultwhen there is none) when a
dedupe key is set, elseid. -
HookHeaders.parsefor receivers: thex-ankusa-id,x-ankusa-source,
x-ankusa-tenant,content-type,x-ankusa-dedupe-key,
x-ankusa-replay-id, andx-ankusa-idempotency-keyheaders of a delivery,
andMissingHookIdErrorwhenx-ankusa-idis absent. -
Signature.verifyfor receivers of a signed HTTP sink: the Standard Webhooks
webhook-id/webhook-timestamp/webhook-signaturecheck, compared with
MessageDigest.isEqual, raisingInvalidSignatureError(code(),
field(), never retryable). -
A gateway
408or429onredeemis aClaimCheckUnavailableError
(retryable), not aClaimRejectedError. -
SourcesClient: the tenant-scoped source API —serverVersion,listSources,
getSource,createSource,updateSource, anddeleteSource. A client built
with anexpectedVersionfetches/healthonce, caches the reported version,
and raisesVersionMismatchErroron every later call that disagrees. -
The error hierarchy: one abstract
AnkusaExceptionper client family
(ClaimCheckError,RoutesError,AdminError,SourcesError), with
retryable()true only on the four*UnavailableErrorclasses. Caller misuse
is anIllegalArgumentExceptioninstead, raised before any request: a base URL
that is not an absolute http(s) URL, a header the JDK client refuses to set, or
a body that cannot be encoded as JSON. -
ClientOptions, carrying the request headers, the timeout (10 s by default,
bounding connect through the last body byte), and theTransport. The default
transport is the JDKHttpClient; inject your own to test, or to drive a
different stack. -
The language-neutral conformance runner (JUnit Jupiter,
ConformanceTest), passing
every vector inconformance/cases.
sdk-rust-v0.5.0
Added
verify_signature(headers, body, secrets, VerifyOptions)verifies the
Standard Webhooks signature an HTTP sink with asecretadds, comparing in
constant time (hmac); failures areInvalidSignatureError(code,
field, never retryable). New dependency:hmac.decode_messageandMessage, for consumers of Ankusa's queue: the inline
(body_base64) and claim (claim) body forms,sha256/size/tenant
validation, forwardedheaders, the shippedidempotency_keyfield, and
Message::idempotency_key(include_replay), which returns it (computing
tenant:source_id:dedupe_key, tenantdefaultwhen none, elseid, only for
a message that predates the field).HookHeadersgainsdedupe_key,replay_id,idempotency_key(the shipped
x-ankusa-idempotency-key) andidempotency_key(include_replay).AdminClientgainscreate_replay,get_replay,list_replays, and
update_replay, with theReplay,ReplayList,ReplaySpec, and
ReplayPatchmodels. Kinds aredlq,archiveandquarantine;
ReplaySpec'sid/since/untilapply toquarantinetoo.
Changed
ClaimCheckClient::redeemreturnsClaimCheckError::Unavailable
(retryable) for a gateway408or429instead ofRejected.
Removed
AdminClient::replay_dead_lettersand itsReplayFilter/Replayedmodels,
replaced by the replay-job API above.
sdk-ruby-v0.5.0
Added
Ankusa.verify_signature(headers, body, secrets, tolerance_seconds: 300, now: nil)
verifies the Standard Webhooks signature an HTTP sink with asecretadds,
comparing withOpenSSL.fixed_length_secure_compare; failures raise
Ankusa::InvalidSignatureError(code,field, never retryable).Ankusa::MessageandAnkusa.decode_message(data), plus
Ankusa::InvalidMessageError(withcode,fieldandretryable?false):
decode and validate the v1 queue message every sink delivers — the inline
(body_base64) and claim-check (claim) body forms,sha256,
dedupe_key,replay_id, and the forwardedheaders, ignoring unknown
keys.Message#idempotency_key/HookHeaders#idempotency_key, the key to store
in a processed-ids table: the key Ankusa shipped (the message's
idempotency_keyfield, or thex-ankusa-idempotency-keyheader), and for a
message or delivery that predates ittenant:source_id:dedupe_key(tenant
defaultwhen there is none) when adedupe_keyis set, elseid, with an
optional#replay:<replay_id>suffix (include_replay: true). The decoded
field stays into_h[:idempotency_key].HookHeaders#dedupe_key/#replay_id/#idempotency_key's field, parsed
fromx-ankusa-dedupe-key/x-ankusa-replay-id/x-ankusa-idempotency-key.AdminClient#create_replay,#get_replay,#list_replaysand
#update_replayfor thePOST/GET/PATCH /v1/replaysreplay-job API (kinds
dlq,archive,quarantine).
Changed
ClaimCheckClient#redeemraisesClaimCheckUnavailableError(retryable)
for a gateway408or429instead ofClaimRejectedError.
Removed
AdminClient#replay_dead_letters, replaced by the replay-job API above.
POST /v1/dlq/replayno longer exists.
sdk-python-v0.5.0
Added
verify_signature(headers, body, secrets, *, tolerance_seconds=300, now=None)
verifies the Standard Webhooks signature an HTTP sink with asecretadds,
comparing in constant time; failures raiseInvalidSignatureError(code,
field,retryable=False).message.Message,message.decode_message(), andmessage.InvalidMessageError
(withcode,fieldandretryable=False): decode and validate the v1
queue message every sink delivers — the inline (body_base64) and
claim-check (claim) body forms,sha256,dedupe_key,replay_id, and
the forwardedheaders, ignoring unknown keys.message.idempotency_key(), the key to store in a processed-ids table: the
key Ankusa shipped (the message'sidempotency_key, or the
x-ankusa-idempotency-keyheader), and for a message or delivery that
predates the fieldtenant:source_id:dedupe_key(tenantdefaultwhen there
is none) whendedupe_keyis set, elseid, with an optional
#replay:<replay_id>suffix (include_replay=True).Message.idempotency_key, decoded likereplay_id, and
HookHeaders.dedupe_key/HookHeaders.replay_id/
HookHeaders.idempotency_key, parsed fromx-ankusa-dedupe-key/
x-ankusa-replay-id/x-ankusa-idempotency-key.AdminClient.create_replay(),get_replay(),list_replays()and
update_replay()for thePOST/GET/PATCH /v1/replaysreplay-job API (kinds
dlq,archive,quarantine).
Changed
ClaimCheckClient.redeem()raisesClaimCheckUnavailableError(retryable)
for a gateway408or429instead ofClaimRejectedError.
Removed
AdminClient.replay_dead_letters(), replaced by the replay-job API above.
POST /v1/dlq/replayno longer exists.
sdk-php-v0.5.0
Added
Webhook\Signature::verify($headers, $body, $secrets, $toleranceSeconds = 300, $now = null)
verifies the Standard Webhooks signature an HTTP sink with asecretadds,
comparing withhash_equals; failures throw
Webhook\InvalidSignatureError($errorCode,$field, never retryable).Message::$idempotencyKeyandHookHeaders::$idempotencyKey, the key Ankusa
ships (the message'sidempotency_key, thex-ankusa-idempotency-key
header).idempotencyKey()on both returns it, and for a message or
delivery that predates it computestenant:source_id:dedupe_key(tenant
defaultwhen there is none) when a dedupe key is set, else the id, plus
#replay:<replay_id>withincludeReplay: true.AdminClient::createReplay(array $spec),getReplay(string $id),
listReplays()andupdateReplay(string $id, array $patch)for replay jobs
(kinddlq,archiveorquarantine).Ankusa\Message\Message::decode(string $data)andInvalidMessageError
($errorCode,$field,isRetryable()always false) for the v1 queue
message (inline or claim body,sha256,dedupeKey,replayId,
headers).HookHeaders::$dedupeKeyand$replayId, fromx-ankusa-dedupe-keyand
x-ankusa-replay-id.
Changed
ClaimCheckClient::redeem()throwsClaimCheckUnavailableError
(retryable) for a gateway408or429instead ofClaimRejectedError.
Removed
AdminClient::replayDeadLetters():POST /v1/dlq/replayno longer exists.
sdk-go-v0.5.0
Added
VerifySignature(header, body, secrets, VerifyOptions)verifies the
Standard Webhooks signature an HTTP sink with asecretadds, comparing
withhmac.Equal; failures are*InvalidSignatureError(Code,Field,
not retryable).DecodeMessageand theMessagetype: decode a v1 queue message with the
body-integrity checks (size_mismatch,integrity,tenant_mismatch) and
thededupe_key/replay_id/idempotency_key/headersfields, raising
InvalidMessageError(Code,Field,Retryable() == false) on any
malformed input. The wire'sidempotency_keyisMessage.ShippedIdempotencyKey
(the field name cannot beIdempotencyKey, which is the helper).Message.IdempotencyKey(includeReplay)and
HookHeaders.IdempotencyKey(includeReplay): the key a consumer dedupes a
delivery on. It is the key Ankusa shipped (idempotency_key/
x-ankusa-idempotency-key); for a message or delivery that predates it, the
helper computestenant:source_id:dedupe_key(tenantdefaultwhen there is
none) when a dedupe key is set, else the id.HookHeaders.DedupeKey,HookHeaders.ReplayID, and
HookHeaders.ShippedIdempotencyKey, read fromx-ankusa-dedupe-key,
x-ankusa-replay-id, andx-ankusa-idempotency-keybyParseHeaders.AdminClient.CreateReplay,GetReplay,ListReplays, andUpdateReplay,
with theReplay,ReplayList,ReplaySpec, andReplayPatchtypes. Kinds
aredlq,archiveandquarantine;ReplaySpec.ID,SinceandUntil
coverquarantinetoo.
Changed
Redeemreturns*ClaimCheckUnavailableError(retryable) for a gateway
408or429instead of*ClaimRejectedError.
Removed
AdminClient.ReplayDeadLetters,ReplayFilter, andReplayed; replay is
now the replay-job API above.
sdk-elixir-v0.5.0
Added
Ankusa.SDK.Signature.verify/4verifies the Standard Webhooks signature an
HTTP sink with asecretadds, comparing with:crypto.hash_equals/2;
failures areAnkusa.SDK.InvalidSignatureError(code,field, never
retryable).Ankusa.SDK.Receivertakes:secret(one or a list) and
:tolerance_seconds, and answers401
{"error":"invalid_signature","code":...}to a delivery that doesn't
verify.Ankusa.SDK.Message.decode/1now carriesdedupe_key,replay_id,
idempotency_keyandheaders, verifies an inline body's length and sha256,
checks a claim's tenant againsttenant_id, and rejects a claim that isn't a
well-formed ref.Ankusa.SDK.InvalidMessageErrorgainscodeandfield.Ankusa.SDK.Idempotency.key/2: the idempotency key for a message, parsed
webhook headers, or a hook. It returns the key Ankusa shipped (the message's
idempotency_key, thex-ankusa-idempotency-keyheader) and, for a message
or delivery that predates it, computestenant:source_id:dedupe_key(tenant
defaultwhen there is none) when a provider event key is present, else the
hook id;include_replay: trueappends the replay id.Ankusa.SDK.Hookcarriesdedupe_key,replay_id,idempotency_keyand
headers;Ankusa.SDK.Webhook.Headersparsesx-ankusa-dedupe-key,
x-ankusa-replay-idandx-ankusa-idempotency-keyand keeps every request
header.Ankusa.SDK.Admin.create_replay/2,get_replay/2,list_replays/1and
update_replay/3for the replay-job API (POST/GET/PATCH /v1/replays;
kindsdlq,archive,quarantine).
Changed
Ankusa.SDK.ClaimCheck.redeem/3returnsClaimCheckUnavailableError
(retryable) for a gateway408or429instead ofClaimRejectedError.
Removed
- The admin client's
replay_dead_lettersfunction;POST /v1/dlq/replayis
gone in favour of replay jobs.
async_api_spex-v0.2.0
Added
use AsyncApiSpex.Schema, fields: [...]derives a schema from an existing
defstruct; every struct key becomes a property andfields:refines it
with a type,required, and adescription.title:anddescription:
describe the schema.use AsyncApiSpex.Channeldeclares a channel, the server it lives on, and
its operation on the module that publishes to it or consumes from it.use AsyncApiSpex.Specimplementsspec/0by assembling a document from
channel modules, listed withchannels:or discovered withotp_app:.AsyncApiSpex.Plug.Viewerserves an HTML page that renders a document in
the browser with the AsyncAPI React component (pinned to 3.2.1 on unpkg;
:bundle_urland:stylesheet_urlpoint it at a self-hosted copy). Mount it
next toAsyncApiSpex.Plug.RenderSpecwithpath:set to that mount.- CI scaffolds a fresh
mix newapp that depends on nothing but this library
andplug, decorates a producer and a consumer, and serves, validates, and
writes the document, so the library is proven outside the repository it
lives in.
Changed
use AsyncApiSpex.Schemawithout:schemaor:fieldsnow raises
"requires exactly one of :schema or :fields".AsyncApiSpex.validate/1is stricter. A document with a component
schema that holds a module that does notuse AsyncApiSpex.Schemanow fails
validate/1andmix async_api_spex.gen.
Fixed
- A schema that references itself no longer makes the resolver loop forever.
ankusa_server-v0.6.0
Added
- Every delivery says how the hook was verified:
Sink.Httpsends
x-ankusa-verification(ok,failedorskipped) and, for a hook a
source accepted despite a failed check (on_verify_failure: accept_flag),
x-ankusa-verification-flagged: true; a queue sink's message carries
verification: {status, flagged}. The admin API's source routes show
trust_url_tenant. idle_timeout_msonrabbitmq,kafka,natsandredissinks: close a
broker connection after that long with no delivery (default 10 minutes,0
never).- Source key
trust_url_tenant(defaultfalse). With
routing: tenant_path, a source withtenant: defaultand
verify: {type: none}files hooks under whatever tenant the URL names:
boot andcheck-confignow refuse it (exit 78, naming the source) unless it
setstrust_url_tenant: true, and an admin API write of one is
400 invalid_source. Onesource_store: persistent/redisalready held
keeps serving, with a warning at boot.config-examples/multi-tenant.yml's
demosource sets it. source_store.type: redis(url,namespace,tick_ms; env
ANKUSA_SOURCE_STORE_URL; aurlwith notypemeansredis): sources
created through the admin API are shared by every node through
ankusa_redis'sAnkusa.SourceStore.Redis. The three keys are rejected on
static/persistent.- YAML keys for everything core added:
admin.gauge_interval_ms,
batcher.max_queue_bytes,dispatch.sink_concurrency,
dispatch.breaker_failures/breaker_open_ms/breaker_max_open_ms,
storage.key_prefix(envANKUSA_STORAGE_KEY_PREFIX),claim_check.store
(a dedicated claim store, same shape asstorageplusroot),
storage.s3.session_token, http sinksecretandmax_response_bytes,
rabbitmq sinkmax_inflight, kafka sinkmax_record_bytes. - A
backupsection (enabled,interval_ms,keep,store— the
claim_check.storeshape; envANKUSA_BACKUP_ENABLED) for core's store
backup: with it on and segments (orbackup.store) in a bucket, a container
started on an empty volume restores the latest backup before it serves, and
exits withstore_restore_failedwhen the bucket can't be read.
check-configrunsAnkusa.Store.Backup.validate_config!/1. docker-entrypoint remote(an IEx shell in the running node) and
docker-entrypoint rpc EXPR. Distribution stays on, bound to loopback with
epmd (rel/vm.args.eex,rel/env.sh.eex);RELEASE_DISTRIBUTION=none
turns it off.- GCP metadata tokens (GCS and Pub/Sub) come from
AnkusaServer.GcpToken,
one cached token fetched single-flight and refreshed before expiry. type: sqssinks (Ankusa.Sink.SQS):queue_url,region,endpoint,
message_group_id,inline_max_bytes,max_message_bytes,timeout_ms,
and optional staticaccess_key_id/secret_access_key/
session_token(else theAWS_*env, IRSA, or the instance role).
reference.ymldocuments it.type: google_pubsubsinks (Ankusa.Sink.GooglePubSub):project,
topic,endpoint,ordering_key,inline_max_bytes,
max_message_bytes,timeout_ms, andauth(metadata|token|
none, defaultmetadata;tokenis required whenauth: token), the
same choicestorage.gcs.authoffers.reference.ymldocuments it.- Every sink now sends the tenant-scoped idempotency key: the message gains
idempotency_key(stillv: 1),Sink.Httpsendsx-ankusa-idempotency-key,
and RabbitMQ, Kafka and NATS sendankusa_idempotency_key. It is
tenant:source_id:dedupe_keywhen the source has a dedupe key, else the hook
id. Consumers should dedupe on it; the SDK helpers read it. dispatch.attempt_timeout_ms(default30000): a delivery attempt that has
not returned after it is killed and counts as a failed attempt
({:attempt_timeout, ms}in the retry error or the DLQ reason), so a hung
sink frees its slot. A value that is not a positive integer fails
check-config.- A
quarantine:section (burst,rate,max_bytes): one quarantine
bucket per source, and a cap on the pen's bytes. A full pen answers
503 quarantine_full; a source over its bucket answers
429 quarantine_rate_limited. verify.secrettakes a list of strings (at most 8), newest first, for a
secret rotation with no cut-over:secret: ["${NEW}", "${OLD}"].- Release held hooks with
POST /v1/replays {"kind":"quarantine"}, and purge
them withDELETE /v1/quarantine. Seedocs/delivery.md#quarantine. ankusa_ingest_refused_total{instance,reason}onGET /metrics: requests
the edge answered without ingesting them (unknown_source,
payload_too_large,body_read_failed,invalid_header).admin.ip,claim_check.ipandroutes.admin.ip: the address each listener
binds (a strict IPv4 or IPv6 literal, default127.0.0.1), and the
ANKUSA_ADMIN_IPandANKUSA_CLAIM_CHECK_IPoverrides.check-config
rejects anything that is not an IP address, naming the key.- Replay jobs:
POST /v1/replays(kind: dlq | archive | quarantine),
GET /v1/replaysandGET|PATCH /v1/replays/{id}. - Source key
dedupe(a preset, or{preset|header|json, ttl_seconds}): a
repeat of an event inside the TTL answers201with the originalidand
"duplicate": true. Source keyforward_headers. wal.publish_timeout_ms(default8000): the deadline every sink must
confirm under withwal.type: none.- Metrics
ankusa_replay_moved_total,ankusa_replay_throttled_totaland
ankusa_quarantine_full_total.
Changed
- The image's
HEALTHCHECKasksGET /ready(docker-entrypoint healthcheck, ingest port first, then admin), so a node whose store refuses
writes reportsunhealthy. - An unresolved
${…}in the file is a load error naming the key (a
substituted value that itself contains${is fine); a static source whose
verifier secret is missing or undecodable failscheck-configand boot. AnkusaServer.GcpTokenanswers:errorto every caller for a second after
a failed metadata fetch, so a queue of callers behind a dead metadata server
costs one request.:os_monruns (disk gauges); onlydisksupis configured. The image adds
coreutils:disksuprunsdf -lk -x squashfs, which busybox'sdf
rejects.GET /asyncapi.json:SinkMessageV1now requiressha256,dedupe_key,
replay_id,idempotency_keyandheaders, andSinkMessageHeadersV1
requiresankusa_idempotency_key. A consumer that validates messages
against the previous document rejects the new fields only if it also
forbids additional properties.- Breaking: an empty
verify.secretis a load error. An empty string, or
an empty element of a secret list (an unset${OLD:-}included), fails
check-configand the boot, naming the key (sources.a.verify.secret[1]: must not be empty). It used to load and verify with the empty HMAC key,
which accepts anything signed with it. - Breaking: a source over its quarantine bucket answers
429 quarantine_rate_limitedwithRetry-After, not401 verification_failed;
a full pen answers503 quarantine_fullwithRetry-After: 60. - The default retry policy retries for about 6 hours:
dispatch.retry
defaults tomax_ms: 300000andmax_attempts: 84(they were30000and
12, about 83 s). A deployment that relied on the old default to
dead-letter within minutes should setmax_attemptsexplicitly. Errors are
not classified yet, so a permanent failure (an HTTP400, say) also takes
every attempt before it reaches the DLQ. - Breaking: the image's admin API (4002) and claim gateway (4001) listen on
127.0.0.1inside the container.-p 127.0.0.1:4002:4002alone no longer
reaches the admin API, and the claim gateway is unreachable from other
containers. SetANKUSA_ADMIN_IP=0.0.0.0andANKUSA_CLAIM_CHECK_IP=0.0.0.0
(oradmin.ipandclaim_check.ip) to listen on the container's interface;
the DockerHEALTHCHECKkeeps working. The compose files, the smoke script
and the examples set it where they publish or share a port. - Breaking: a request with a header name or value outside visible ASCII (and
space and tab in a value) is400 invalid_header. It used to be acked and
then dead-lettered when a sink could not carry it; see the core changelog. - Breaking for anything that reads
GET /v1/config: adapter options are
shown only under known non-secret keys, and every other value (a
sas_token, aprivate_key, an NATSjwt) is"[REDACTED]"; URL query
values are redacted too.print-configuses the same view. - Breaking for dashboards: refused requests are no longer on
ankusa_ingest_requests_total. Requests for sources that do not exist no
longer count underankusa_ingest_requests_total{outcome="unknown_source"};
a panel that sums that counter for all traffic must add
ankusa_ingest_refused_total. - Breaking:
POST /v1/dlq/replayis removed. Use
POST /v1/replays {"kind":"dlq"}. Sink.Httpforwards the provider's request headers by default (per the
source'sforward_headers) and addsx-ankusa-dedupe-keyand
x-ankusa-replay-idwhen present.wal.type: nonepublishes to all of a source's sinks concurrently under
wal.publish_timeout_ms; any failure or timeout is a503.- Breaking: every RabbitMQ publish is
mandatory. A publish to an
exchange with no bound queue now retries and then dead-letters instead of
silently answering201. RabbitMQ also sets AMQPmessage_idto the hook
id. - NATS sets
Nats-Msg-Idon every publish (the hookid, or
id:replay:<replay_id>on a replay).
Fixed
GET /metricsno longer grows with unauthenticated traffic: requests for
sources that do not exist are oneankusa_ingest_refused_totalseries, not
asource_idseries per URL.- A
Content-Lengthabovehttp.max_body_bytes, or a source th...
ankusa_redis-v0.5.0
Added
Ankusa.SourceStore.Redis: API-managed, tenant-scoped sources kept in Redis
and shared by every node configured with the samenamespace(a hash of
sources and a version counter; Lua-scripted writes; pub/sub invalidation with
atick_mssafety net). Reads go to an ETS mirror that survives a restart of
the store's state process. Seeds stay config-only. A first boot against an
unreachable Redis fails; a write during an outage answers
{:error, :store_unavailable}(the admin API's503 store_unavailable).Ankusa.Sink.Redis::idle_timeout_ms(default600_000,0never, at
least:publish_timeout_msplus 1 s): a connection with no delivery for that
long is closed (Ankusa.Sink.Reaper, requires core's) and the next delivery
opens a new one.
Changed
- Requires
ankusa ~> 0.6(the ETS route snapshot API, and the source table
SourceStore.Redisshares withSourceStore.Persistent); publish core first. - The Redis route store keeps serving its last known table when the
namespace's version key disappears (a flushed or evicted Redis) instead of
publishing an empty table that rejects every webhook; it logs a warning
once and reloads when the namespace is written again. It publishes through
core's ETS route snapshot and drops retired generations on request. - The Redis sink's connection dials on its own (
sync_connect: false), so a
dead Redis never blocks the supervisor that starts it. Ankusa.Routes.Store.RedisandAnkusa.SourceStore.Redisstart with
[instance: instance](core's new store contract) and read their options
fromAnkusa.config(instance): no child spec of theirs carries the config,
the seeds or the decoder. Their Redix connections get the URL's password as
Redix's{m, f, a}form, so neither a child spec nor Redix's state holds it.SourceStore.Redisrefuses a write of a shared source with no verifier
under a resolver that takes the tenant from the request unless it sets
trust_url_tenant: true({:error, :invalid, message}), and names one
already in Redis in a warning at first boot.- A route store restarted after the instance has published a table resumes from
that table without waiting on Redis (its connections dial in the background),
so a store that crashes during a Redis outage does not hold the edge down. It
loads once the subscription is confirmed and on every tick. A first boot
still fails loudly. Ankusa.Sink.Redisregisters its connection under a digest of the URL
instead of the URL, and starts Redix with the URL's password as an
{m, f, a}over a closure: the password is in no registry key, child spec
or Redix state.