daytona 0.200.0 → 0.200.1

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: de70d9e56ce97468d9ef3bcf8c7dbe0b70f74ee7e64a7ac1d0e8642857a3c257
4
- data.tar.gz: 3cec447b03a65f42bcd84c49e719cd8771a1404f496384afd5e797b429e47fb1
3
+ metadata.gz: 714770a5cd03bbfb27825fd9c018333d0377db70c0ce32b7ed484ff62946b2da
4
+ data.tar.gz: 1ea5e87028fffadb41659ceb7d76e6ec635ce2801e27f53bf1319d02b66ea6a7
5
5
  SHA512:
6
- metadata.gz: 91c94980be6003996bbbf4b447b170e9165cbc3a47e78c47f91886f28accd5f7b083b04903fc59dede9bbe56ccdcaf9957da73e07202e762ca928203274b2c8b
7
- data.tar.gz: db39e610c03a011d4c2468475393ca7b7bd17cc1087dbb0edd46c9fc6959da18e0ca61d861485ca0b27faa8ccb4061f631ec44c97d90d8e6bea7db75317db4e3
6
+ metadata.gz: 9f8bec4d5a367c8c1f2cc48bab297903dd2b75f03460c0c72728842a639da4dd00c781ec5668f64aef5375af1955f3e1cb6221577dfa4d3fd237c97fbb6056e2
7
+ data.tar.gz: 19a3b2b8bafb2c5a9fc14777411de38abe6a6ac17b1c0d132af9334142ab915ef3aa325aa8184e1ac999df7e0c867a47e8d3691975b09be3ca6a98a80584b8b8
@@ -6,14 +6,29 @@
6
6
  require 'securerandom'
7
7
 
8
8
  module Daytona
9
+ # Tracks dispatcher subscriptions with TTL auto-expiry.
10
+ #
11
+ # All subscriptions share ONE lazily started expiry worker thread instead of a
12
+ # dedicated sleeping thread per subscription, so listing many sandboxes costs
13
+ # one thread total, not one thread each
14
+ # (https://github.com/daytona/clients/issues/108). #refresh only bumps the
15
+ # subscription deadline — it never creates or kills threads.
9
16
  class EventSubscriptionManager
10
17
  SUBSCRIPTION_TTL = 300
11
18
  private_constant :SUBSCRIPTION_TTL
12
19
 
13
- def initialize(dispatcher = nil)
20
+ def initialize(dispatcher = nil, ttl_seconds: SUBSCRIPTION_TTL)
14
21
  @dispatcher = dispatcher
22
+ @ttl = ttl_seconds
15
23
  @subscriptions = {}
24
+ # Deadline-ordered [expires_at, sub_id] entries. Entries are lazily
25
+ # invalidated: a popped entry is discarded when its subscription is gone,
26
+ # or re-queued at the current deadline when it was refreshed meanwhile.
27
+ # Invariant: every live subscription has at least one queue entry.
28
+ @expiry_queue = []
16
29
  @mutex = Mutex.new
30
+ @cond = ConditionVariable.new
31
+ @worker = nil
17
32
  @closed = false
18
33
  end
19
34
 
@@ -24,7 +39,6 @@ module Daytona
24
39
 
25
40
  unsubscribe = @dispatcher.subscribe(resource_id, events:, &handler)
26
41
  sub_id = SecureRandom.hex(16)
27
- timer_to_stop = nil
28
42
  rollback_unsubscribe = nil
29
43
 
30
44
  @mutex.synchronize do
@@ -34,8 +48,11 @@ module Daytona
34
48
  next
35
49
  end
36
50
 
37
- @subscriptions[sub_id] = { unsubscribe:, timer: nil }
38
- timer_to_stop = start_timer_locked(sub_id)
51
+ expires_at = monotonic_now + @ttl
52
+ @subscriptions[sub_id] = { unsubscribe:, expires_at: }
53
+ push_entry_locked(expires_at, sub_id)
54
+ ensure_worker_locked
55
+ @cond.signal
39
56
  end
40
57
 
41
58
  if rollback_unsubscribe
@@ -43,44 +60,36 @@ module Daytona
43
60
  return nil
44
61
  end
45
62
 
46
- stop_thread(timer_to_stop)
47
63
  sub_id
48
64
  end
49
65
 
50
66
  def refresh(sub_id)
51
67
  @mutex.synchronize do
52
- # Reject after shutdown to prevent use-after-close
53
68
  return false if @closed
54
- end
55
-
56
- timer_to_stop = nil
57
69
 
58
- @mutex.synchronize do
59
- return false unless @subscriptions.key?(sub_id)
70
+ subscription = @subscriptions[sub_id]
71
+ return false unless subscription
60
72
 
61
- timer_to_stop = start_timer_locked(sub_id)
73
+ # No queue entry or worker wake-up needed: when the old deadline pops,
74
+ # the worker sees the newer expires_at and re-queues the subscription.
75
+ subscription[:expires_at] = monotonic_now + @ttl
76
+ true
62
77
  end
63
-
64
- stop_thread(timer_to_stop)
65
- true
66
78
  end
67
79
 
68
80
  def unsubscribe(sub_id)
69
- subscription = nil
70
- timer = nil
71
-
72
- @mutex.synchronize do
73
- subscription = @subscriptions.delete(sub_id)
74
- if subscription
75
- timer = subscription[:timer]
76
- subscription[:timer] = nil
81
+ subscription = @mutex.synchronize do
82
+ removed = @subscriptions.delete(sub_id)
83
+ # The stale queue entry is discarded when the worker pops it — except
84
+ # when it was the last subscription: drop the leftovers and wake the
85
+ # worker so it exits now instead of idling until the old deadline.
86
+ if removed && @subscriptions.empty?
87
+ @expiry_queue.clear
88
+ @cond.signal
77
89
  end
90
+ removed
78
91
  end
79
-
80
- return unless subscription
81
-
82
- stop_thread(timer)
83
- subscription[:unsubscribe].call
92
+ subscription&.dig(:unsubscribe)&.call
84
93
  end
85
94
 
86
95
  def shutdown
@@ -90,55 +99,96 @@ module Daytona
90
99
  @closed = true
91
100
  subscriptions = @subscriptions.values
92
101
  @subscriptions = {}
102
+ @expiry_queue.clear
103
+ @cond.broadcast
93
104
  end
94
105
 
95
- subscriptions.each do |subscription|
96
- stop_thread(subscription[:timer])
97
- subscription[:unsubscribe].call
98
- end
106
+ subscriptions.each { |subscription| subscription[:unsubscribe].call }
99
107
  end
100
108
 
101
109
  private
102
110
 
103
- def start_timer_locked(sub_id)
104
- subscription = @subscriptions[sub_id]
105
- return unless subscription
111
+ def monotonic_now
112
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
113
+ end
106
114
 
107
- previous_timer = cancel_timer_locked(sub_id)
108
- timer = Thread.new do
109
- sleep(SUBSCRIPTION_TTL)
115
+ # Caller must hold @mutex.
116
+ def push_entry_locked(expires_at, sub_id)
117
+ index = @expiry_queue.bsearch_index { |entry| entry[0] > expires_at } || @expiry_queue.length
118
+ @expiry_queue.insert(index, [expires_at, sub_id])
119
+ end
110
120
 
111
- unsubscribe = @mutex.synchronize do
112
- current_subscription = @subscriptions[sub_id]
113
- next unless current_subscription && current_subscription[:timer] == Thread.current
121
+ # Caller must hold @mutex. The alive? check is a safety net: a worker that
122
+ # crashed for any reason leaves a dead-but-truthy reference, and a plain nil
123
+ # check would then block replacements forever, silently disabling expiry.
124
+ def ensure_worker_locked
125
+ return if @worker&.alive?
114
126
 
115
- @subscriptions.delete(sub_id)&.dig(:unsubscribe)
116
- end
127
+ @worker = Thread.new { expiry_loop }
128
+ @worker.name = 'daytona-subscription-expiry'
129
+ @worker.abort_on_exception = false
130
+ end
117
131
 
118
- unsubscribe&.call
132
+ def expiry_loop
133
+ loop do
134
+ expired = collect_expired
135
+ return if expired.nil?
136
+
137
+ expired.each do |subscription|
138
+ subscription[:unsubscribe].call
139
+ rescue StandardError
140
+ # One failing dispatcher unsubscribe must not kill the shared worker
141
+ # and silently stop expiry for every other subscription.
142
+ nil
143
+ end
119
144
  end
120
- timer.abort_on_exception = false
121
- subscription[:timer] = timer
122
- previous_timer
123
145
  end
124
146
 
125
- def cancel_timer_locked(sub_id)
126
- subscription = @subscriptions[sub_id]
127
- return unless subscription
147
+ # Blocks until at least one subscription expires. Returns nil when the
148
+ # worker should exit (shutdown, or no live subscriptions remain).
149
+ def collect_expired
150
+ @mutex.synchronize do
151
+ expired = []
152
+
153
+ while expired.empty?
154
+ return nil if @closed
155
+
156
+ if @expiry_queue.empty?
157
+ # No live subscriptions remain (see queue invariant above):
158
+ # exit and let the next subscribe start a fresh worker.
159
+ @worker = nil
160
+ return nil
161
+ end
162
+
163
+ now = monotonic_now
164
+ deadline = @expiry_queue.first[0]
165
+ if deadline > now
166
+ @cond.wait(@mutex, deadline - now)
167
+ next
168
+ end
169
+
170
+ drain_due_entries_locked(now, expired)
171
+ end
128
172
 
129
- timer = subscription[:timer]
130
- subscription[:timer] = nil
131
- timer
173
+ expired
174
+ end
132
175
  end
133
176
 
134
- def stop_thread(thread)
135
- return unless thread
136
- return if thread == Thread.current
177
+ # Caller must hold @mutex.
178
+ def drain_due_entries_locked(now, expired)
179
+ while !@expiry_queue.empty? && @expiry_queue.first[0] <= now
180
+ _, sub_id = @expiry_queue.shift
181
+ subscription = @subscriptions[sub_id]
182
+ next unless subscription
137
183
 
138
- thread.kill
139
- thread.join
140
- rescue StandardError
141
- nil
184
+ if subscription[:expires_at] > now
185
+ push_entry_locked(subscription[:expires_at], sub_id)
186
+ next
187
+ end
188
+
189
+ @subscriptions.delete(sub_id)
190
+ expired << subscription
191
+ end
142
192
  end
143
193
  end
144
194
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Daytona
7
7
  module Sdk
8
- VERSION = '0.200.0'
8
+ VERSION = '0.200.1'
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: daytona
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.200.0
4
+ version: 0.200.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daytona Platforms Inc.
@@ -85,42 +85,42 @@ dependencies:
85
85
  requirements:
86
86
  - - '='
87
87
  - !ruby/object:Gem::Version
88
- version: 0.200.0
88
+ version: 0.200.1
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - '='
94
94
  - !ruby/object:Gem::Version
95
- version: 0.200.0
95
+ version: 0.200.1
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: daytona_api_client
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - '='
101
101
  - !ruby/object:Gem::Version
102
- version: 0.200.0
102
+ version: 0.200.1
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - '='
108
108
  - !ruby/object:Gem::Version
109
- version: 0.200.0
109
+ version: 0.200.1
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: daytona_toolbox_api_client
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - '='
115
115
  - !ruby/object:Gem::Version
116
- version: 0.200.0
116
+ version: 0.200.1
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - '='
122
122
  - !ruby/object:Gem::Version
123
- version: 0.200.0
123
+ version: 0.200.1
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: dotenv
126
126
  requirement: !ruby/object:Gem::Requirement