Class: Insika::Tick

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/tick.rb

Overview

The periodic tick: durability stops waiting for a reboot. One pass does two things, in this order:

1. DRAIN the outbox (`ChannelDelivery#sweep`) — replies a previous pass
 (or process) recorded and never claimed. Ungated: every record carries
 its own transactional claim, so N workers draining is safe.
2. SWEEP stale orphaned tasks (`Recovery#run(stale_after:)`) — gated by a
 bucketed claim (`Recovery.claim_sweep` on "tick:<epoch/interval>"), so
 exactly one worker per window sweeps. The staleness threshold is the
 liveness gate: a live :running turn is bounded by turn_timeout, so
 anything untouched past it cannot be alive.

It is NOT a job queue: no schedules, no priorities, no fan-out. The refinement hook once pictured here is dropped by merit — docs/REFINEMENT.md's "no scheduler in the engine" stands.

Constant Summary collapse

DEFAULT_INTERVAL =

60s: a customer waiting on WhatsApp is the deadline. 900s = 3x the default turn_timeout (300s) — the rule, not the number: the threshold must exceed the deployment's largest turn_timeout, or the sweep would judge live turns orphaned.

60
DEFAULT_STALE_AFTER =
900
SCOPE =
"tick"
KEY =
"claim"

Instance Method Summary collapse

Constructor Details

#initialize(store:, recovery:, channel_delivery:, logger: nil, interval: DEFAULT_INTERVAL, stale_after: DEFAULT_STALE_AFTER, sleeper: nil) ⇒ Tick

Returns a new instance of Tick.



32
33
34
35
36
37
38
39
40
41
42
# File 'lib/insika/tick.rb', line 32

def initialize(store:, recovery:, channel_delivery:, logger: nil,
               interval: DEFAULT_INTERVAL, stale_after: DEFAULT_STALE_AFTER,
               sleeper: nil)
  @store = store
  @recovery = recovery
  @channel_delivery = channel_delivery
  @logger = logger
  @interval = interval.to_i
  @stale_after = stale_after.to_i
  @sleeper = sleeper || method(:default_sleep)
end

Instance Method Details

#enabled?Boolean

Returns:

  • (Boolean)


44
# File 'lib/insika/tick.rb', line 44

def enabled? = @interval.positive?

#run_onceObject

One pass, pure (no reactor needed): the serving loop calls it on a timer, specs call it directly. A StoreError propagates to the loop, which logs and keeps ticking.



49
50
51
52
53
54
55
56
# File 'lib/insika/tick.rb', line 49

def run_once
  drained = @channel_delivery ? @channel_delivery.sweep : { dispatched: [] }
  summary = { dispatched: drained[:dispatched], resumed: [], failed: [] }
  return summary unless claim_window

  result = @recovery.run(stale_after: @stale_after)
  summary.merge(resumed: result[:resumed], failed: result[:failed])
end

#start(parent:) ⇒ Object

The loop, spawned as a child of the turn supervisor (the tick lives on the supervisor fiber — every serving arm gets it the moment supervised = true matters, with no arm edits). A failing pass logs and the loop continues: a sweeper that dies silently is the outage it exists to prevent. Restartable: when the supervisor is recreated its children died with it, so a stopped task is not a running one.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/insika/tick.rb', line 64

def start(parent:)
  return false unless enabled?
  return true if @task&.running?

  @task = parent.async do |t|
    t.annotate("insika-tick")
    loop do
      @sleeper.call(@interval)
      run_once
    rescue StandardError => e
      log(:warn, "tick failed: #{e.class}: #{e.message}")
    end
  end
  true
end