lex-tick 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec291267e8b58c191d7031607b05b5f6f09081bb5cf3eeb797dcecf46237419d
4
- data.tar.gz: 709792c3d29424ebaed1cf5bdb1bd914b7a8723b0f693a37d896e4a35d0836de
3
+ metadata.gz: 39bbb25a91bd1c83e768654add47baa2a81e429635a223a88fa9d23df45f7c84
4
+ data.tar.gz: 68b89d858df59af162e15c60b1a8e8f416e6b9e28ad6aa2b2ced4d51beade459
5
5
  SHA512:
6
- metadata.gz: b157b24e3ecffc0ea09c999e01b4d5e7921ff1170dc480926923193453d89a3bdbcc64846379c456933ddd6ef7a2073118f89ae5d3fed6003a7a8f6f2eeb29ae
7
- data.tar.gz: 200d06d265ef64b11bace9864c267a7997900421fd59ebf5c114edf8eb245a5929e1c31942055c6040fdbbe16b836f91a83d264271fd2db9e0d8a582b83d9ac9
6
+ metadata.gz: 9ecf69a533a7e2781dd9253d1d6575fe58615c4b189e33aac1950d84b605710f43ee8e351c3483886bdcc9a35b54ca908ad1d62326e4ab8339a07144bfdcc12c
7
+ data.tar.gz: 26911dfa59621334936c5a197b32eec2f6a918c3d23bab1fe35123342750759b7a6a4fa945b3d05ed726d6cc9541585efccf7dd36e5114f2877a50ce1a421248
@@ -10,10 +10,11 @@ module Legion
10
10
  # Cortex wires phase_handlers from all agentic extensions
11
11
  # and calls execute_tick with real handlers instead of empty ones.
12
12
  # To use tick standalone (without cortex), re-enable this actor.
13
- class Tick < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
13
+ class Tick < Legion::Extensions::Actors::Every
14
14
  def initialize(**opts)
15
15
  return unless enabled?
16
16
 
17
+ apply_initial_jitter
17
18
  super
18
19
  end
19
20
 
@@ -52,6 +53,15 @@ module Legion
52
53
  def args
53
54
  { signals: [], phase_handlers: {} }
54
55
  end
56
+
57
+ private
58
+
59
+ def apply_initial_jitter
60
+ return unless Helpers::Jitter.jitter_enabled?
61
+
62
+ offset = Helpers::Jitter.deterministic_jitter(self.class.name.to_s, time)
63
+ sleep(offset) if offset.positive?
64
+ end
55
65
  end
56
66
  end
57
67
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Tick
6
+ module Helpers
7
+ module Jitter
8
+ MAX_JITTER_CAP = 900 # seconds (15 minutes)
9
+
10
+ module_function
11
+
12
+ # Returns a deterministic integer jitter offset (seconds) for the given task name
13
+ # and interval. The offset is in the range [0, max_jitter) where max_jitter is
14
+ # 10% of the interval, capped at MAX_JITTER_CAP (15 minutes).
15
+ #
16
+ # The same task_name always produces the same offset, so all nodes handling the
17
+ # same named task will sleep the same initial amount — preventing thundering herd
18
+ # while keeping execution predictable.
19
+ def deterministic_jitter(task_name, interval_seconds)
20
+ max_jitter = [interval_seconds * 0.1, MAX_JITTER_CAP].min.to_i
21
+ return 0 if max_jitter < 1
22
+
23
+ task_name.to_s.hash.abs % max_jitter
24
+ end
25
+
26
+ # Returns true when jitter is enabled via settings (default: true).
27
+ def jitter_enabled?
28
+ setting = begin
29
+ Legion::Settings[:tick]
30
+ rescue StandardError => _e
31
+ nil
32
+ end
33
+ return true if setting.nil?
34
+
35
+ tick_hash = setting.is_a?(Hash) ? setting : nil
36
+ return true if tick_hash.nil?
37
+
38
+ tick_hash.fetch(:jitter_enabled, true)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Tick
6
- VERSION = '0.1.10'
6
+ VERSION = '0.1.11'
7
7
  end
8
8
  end
9
9
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'legion/extensions/tick/version'
4
4
  require 'legion/extensions/tick/helpers/constants'
5
+ require 'legion/extensions/tick/helpers/jitter'
5
6
  require 'legion/extensions/tick/helpers/state'
6
7
  require 'legion/extensions/tick/runners/orchestrator'
7
8
 
@@ -16,6 +16,9 @@ $LOADED_FEATURES << 'legion/extensions/actors/every'
16
16
  require 'legion/extensions/tick/actors/tick'
17
17
 
18
18
  RSpec.describe Legion::Extensions::Tick::Actor::Tick do
19
+ # Prevent real sleep calls in all examples; jitter tests override this per-context
20
+ before { allow_any_instance_of(described_class).to receive(:sleep) }
21
+
19
22
  subject(:actor) { described_class.new }
20
23
 
21
24
  describe '#initialize' do
@@ -101,4 +104,47 @@ RSpec.describe Legion::Extensions::Tick::Actor::Tick do
101
104
  expect(actor.args).to eq({ signals: [], phase_handlers: {} })
102
105
  end
103
106
  end
107
+
108
+ describe 'initial jitter behavior' do
109
+ context 'when jitter is enabled and offset is positive' do
110
+ before do
111
+ allow(Legion::Extensions::Tick::Helpers::Jitter).to receive(:jitter_enabled?).and_return(true)
112
+ allow(Legion::Extensions::Tick::Helpers::Jitter).to receive(:deterministic_jitter).and_return(5)
113
+ end
114
+
115
+ it 'sleeps for the jitter offset during initialization' do
116
+ slept = nil
117
+ allow_any_instance_of(described_class).to receive(:sleep) { |_obj, secs| slept = secs }
118
+ described_class.new
119
+ expect(slept).to eq(5)
120
+ end
121
+ end
122
+
123
+ context 'when jitter offset is zero' do
124
+ before do
125
+ allow(Legion::Extensions::Tick::Helpers::Jitter).to receive(:jitter_enabled?).and_return(true)
126
+ allow(Legion::Extensions::Tick::Helpers::Jitter).to receive(:deterministic_jitter).and_return(0)
127
+ end
128
+
129
+ it 'does not call sleep' do
130
+ sleep_called = false
131
+ allow_any_instance_of(described_class).to receive(:sleep) { sleep_called = true }
132
+ described_class.new
133
+ expect(sleep_called).to be false
134
+ end
135
+ end
136
+
137
+ context 'when jitter is disabled' do
138
+ before do
139
+ allow(Legion::Extensions::Tick::Helpers::Jitter).to receive(:jitter_enabled?).and_return(false)
140
+ end
141
+
142
+ it 'does not call sleep' do
143
+ sleep_called = false
144
+ allow_any_instance_of(described_class).to receive(:sleep) { sleep_called = true }
145
+ described_class.new
146
+ expect(sleep_called).to be false
147
+ end
148
+ end
149
+ end
104
150
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Tick::Helpers::Jitter do
6
+ describe '.deterministic_jitter' do
7
+ it 'returns the same value for the same task name and interval' do
8
+ a = described_class.deterministic_jitter('my_task', 60)
9
+ b = described_class.deterministic_jitter('my_task', 60)
10
+ expect(a).to eq(b)
11
+ end
12
+
13
+ it 'returns different values for different task names' do
14
+ a = described_class.deterministic_jitter('task_alpha', 600)
15
+ b = described_class.deterministic_jitter('task_beta', 600)
16
+ # Different names should almost always differ (hash collision is astronomically rare)
17
+ # but we assert the function is at least defined and returns integers
18
+ expect(a).to be_a(Integer)
19
+ expect(b).to be_a(Integer)
20
+ end
21
+
22
+ it 'returns a value within the 10% bound of the interval' do
23
+ interval = 600
24
+ max_jitter = (interval * 0.1).to_i
25
+ jitter = described_class.deterministic_jitter('interval_actor', interval)
26
+ expect(jitter).to be >= 0
27
+ expect(jitter).to be < max_jitter
28
+ end
29
+
30
+ it 'caps jitter at MAX_JITTER_CAP (900 seconds) regardless of interval' do
31
+ jitter = described_class.deterministic_jitter('large_interval_actor', 100_000)
32
+ expect(jitter).to be < described_class::MAX_JITTER_CAP
33
+ end
34
+
35
+ it 'returns 0 for very small intervals where 10% rounds to < 1 second' do
36
+ expect(described_class.deterministic_jitter('tiny_task', 5)).to eq(0)
37
+ expect(described_class.deterministic_jitter('tiny_task', 1)).to eq(0)
38
+ end
39
+
40
+ it 'returns 0 for a zero interval' do
41
+ expect(described_class.deterministic_jitter('zero_task', 0)).to eq(0)
42
+ end
43
+
44
+ it 'accepts a symbol task name without raising' do
45
+ expect { described_class.deterministic_jitter(:symbol_task, 120) }.not_to raise_error
46
+ end
47
+
48
+ it 'produces an integer result' do
49
+ result = described_class.deterministic_jitter('my_task', 300)
50
+ expect(result).to be_a(Integer)
51
+ end
52
+ end
53
+
54
+ describe '.jitter_enabled?' do
55
+ context 'when Legion::Settings is not available or returns nil' do
56
+ before do
57
+ allow(Legion::Settings).to receive(:[]).with(:tick).and_return(nil)
58
+ end
59
+
60
+ it 'defaults to true' do
61
+ expect(described_class.jitter_enabled?).to be true
62
+ end
63
+ end
64
+
65
+ context 'when settings return a hash with jitter_enabled: true' do
66
+ before do
67
+ allow(Legion::Settings).to receive(:[]).with(:tick).and_return({ jitter_enabled: true })
68
+ end
69
+
70
+ it 'returns true' do
71
+ expect(described_class.jitter_enabled?).to be true
72
+ end
73
+ end
74
+
75
+ context 'when settings return a hash with jitter_enabled: false' do
76
+ before do
77
+ allow(Legion::Settings).to receive(:[]).with(:tick).and_return({ jitter_enabled: false })
78
+ end
79
+
80
+ it 'returns false' do
81
+ expect(described_class.jitter_enabled?).to be false
82
+ end
83
+ end
84
+
85
+ context 'when settings raise a StandardError' do
86
+ before do
87
+ allow(Legion::Settings).to receive(:[]).with(:tick).and_raise(StandardError)
88
+ end
89
+
90
+ it 'defaults to true' do
91
+ expect(described_class.jitter_enabled?).to be true
92
+ end
93
+ end
94
+
95
+ context 'when settings return a non-hash value' do
96
+ before do
97
+ allow(Legion::Settings).to receive(:[]).with(:tick).and_return('invalid')
98
+ end
99
+
100
+ it 'defaults to true' do
101
+ expect(described_class.jitter_enabled?).to be true
102
+ end
103
+ end
104
+ end
105
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-tick
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -121,12 +121,14 @@ files:
121
121
  - lib/legion/extensions/tick/actors/tick.rb
122
122
  - lib/legion/extensions/tick/client.rb
123
123
  - lib/legion/extensions/tick/helpers/constants.rb
124
+ - lib/legion/extensions/tick/helpers/jitter.rb
124
125
  - lib/legion/extensions/tick/helpers/state.rb
125
126
  - lib/legion/extensions/tick/runners/orchestrator.rb
126
127
  - lib/legion/extensions/tick/version.rb
127
128
  - spec/legion/extensions/tick/actors/tick_spec.rb
128
129
  - spec/legion/extensions/tick/client_spec.rb
129
130
  - spec/legion/extensions/tick/helpers/constants_spec.rb
131
+ - spec/legion/extensions/tick/helpers/jitter_spec.rb
130
132
  - spec/legion/extensions/tick/helpers/state_spec.rb
131
133
  - spec/legion/extensions/tick/runners/orchestrator_open_inference_spec.rb
132
134
  - spec/legion/extensions/tick/runners/orchestrator_spec.rb