Class: Insika::InboundLog
- Inherits:
-
Object
- Object
- Insika::InboundLog
- Defined in:
- lib/insika/inbound_log.rb
Overview
Short-lived memory of inbound event ids, so a platform's retry does not become a second LLM turn and a second reply.
Every messaging platform retries a webhook it did not see acked in time, and a relay consumer that hands us its own queue does the same. Without this, the cost of one flaky ack is a duplicated turn (paid for) and a duplicated answer (visible to the customer). With it, the retry finds the id and gets the SAME task back — the caller learns it already sent this, which is a different fact from "your message was merged into someone else's turn".
The id is the CALLER's (wamid.…, a Slack event id, whatever the consumer's
queue uses). A caller that cannot supply a stable one gets at-least-once turns
and is told so in the docs — the engine does not hash the content and call
that dedup, because two customers legitimately typing "oi" one second apart is
not a duplicate.
TTL, not forever: this is a retry window, not an audit log. An expired entry is
deleted lazily on read; sweep (boot) clears whatever nobody read back.
Constant Summary collapse
- SCOPE =
"inbound"- KEY_PREFIX =
"inbound:"- DEFAULT_TTL =
24h — longer than any platform's retry schedule
86_400
Instance Method Summary collapse
-
#find(key) ⇒ Object
-> the task id this event already produced, or nil (never seen, or the window closed).
-
#initialize(store:, ttl: DEFAULT_TTL, clock: -> { Time.now.utc }) ⇒ InboundLog
constructor
A new instance of InboundLog.
-
#record(key, task_id) ⇒ Object
Remembers that
keyproducedtask_id. -
#sweep ⇒ Object
Boot housekeeping: drops the entries nobody came back for.
Constructor Details
#initialize(store:, ttl: DEFAULT_TTL, clock: -> { Time.now.utc }) ⇒ InboundLog
Returns a new instance of InboundLog.
30 31 32 33 34 |
# File 'lib/insika/inbound_log.rb', line 30 def initialize(store:, ttl: DEFAULT_TTL, clock: -> { Time.now.utc }) @store = store @ttl = ttl.to_i @clock = clock end |
Instance Method Details
#find(key) ⇒ Object
-> the task id this event already produced, or nil (never seen, or the window closed). An expired entry is removed as it is read: the next identical id is honestly a new message by then.
39 40 41 42 43 44 45 46 47 48 |
# File 'lib/insika/inbound_log.rb', line 39 def find(key) record = @store.get(SCOPE, key_for(key)) return nil if record.nil? if expired?(record) @store.delete(SCOPE, key_for(key)) return nil end record["task_id"] end |
#record(key, task_id) ⇒ Object
Remembers that key produced task_id. Last write wins, like every other
store — a re-record inside the window just refreshes the expiry.
52 53 54 55 56 57 58 59 |
# File 'lib/insika/inbound_log.rb', line 52 def record(key, task_id) @store.set(SCOPE, key_for(key), { "key" => key.to_s, "task_id" => task_id&.to_s, "expires_at" => (@clock.call + @ttl).iso8601 }) task_id end |
#sweep ⇒ Object
Boot housekeeping: drops the entries nobody came back for. -> count removed.
62 63 64 65 66 67 68 69 |
# File 'lib/insika/inbound_log.rb', line 62 def sweep @store.list(SCOPE, KEY_PREFIX).count do |key| record = @store.get(SCOPE, key) next false unless record && expired?(record) @store.delete(SCOPE, key) end end |