daytona 0.197.0 → 0.198.0
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 +4 -4
- data/lib/daytona/common/event_dispatcher.rb +326 -0
- data/lib/daytona/common/event_subscription_manager.rb +144 -0
- data/lib/daytona/common/socketio_client.rb +289 -0
- data/lib/daytona/config.rb +21 -0
- data/lib/daytona/daytona.rb +34 -4
- data/lib/daytona/file_system.rb +1 -1
- data/lib/daytona/lsp_server.rb +1 -1
- data/lib/daytona/sandbox.rb +386 -111
- data/lib/daytona/sdk/version.rb +1 -1
- data/lib/daytona/sdk.rb +5 -2
- data/project.json +4 -3
- metadata +22 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e2003b4293b8ee69d171ad86402151e2924b814022649097deb51da19da6ff58
|
|
4
|
+
data.tar.gz: 30884a506c2dd332c804f995dd2d3afc654cf75b0fa66e4bf54cdd4a38909c12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4627f864da4ae028a19d856a014da0f3513e30a5a8c7299148d89d815a82b13a2c0de3bc8fc3dc4e96ca7a455ee9ef9e291fe71dd58f57e12af8e85ebcbd3d1
|
|
7
|
+
data.tar.gz: c8b07b46dfb64043794af1d4d33db1ca3ac49c1271ebde8eae3b2c9e05ca66030e780d2fbb5cede035c564c03a69f0abba68d87ff098bac36d198fe3ab1d2636
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Daytona Platforms Inc.
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require_relative 'socketio_client'
|
|
7
|
+
|
|
8
|
+
module Daytona
|
|
9
|
+
# Manages a Socket.IO connection and dispatches events to per-resource handlers.
|
|
10
|
+
# Generic — works for sandboxes, volumes, snapshots, runners, etc.
|
|
11
|
+
class EventDispatcher
|
|
12
|
+
DISCONNECT_DELAY = 30
|
|
13
|
+
|
|
14
|
+
# @param api_url [String]
|
|
15
|
+
# @param token [String]
|
|
16
|
+
# @param organization_id [String, nil]
|
|
17
|
+
def initialize(api_url:, token:, organization_id: nil, source: nil, sdk_version: nil)
|
|
18
|
+
@api_url = api_url
|
|
19
|
+
@token = token
|
|
20
|
+
@organization_id = organization_id
|
|
21
|
+
@source = source
|
|
22
|
+
@sdk_version = sdk_version
|
|
23
|
+
@client = nil
|
|
24
|
+
@connected = false
|
|
25
|
+
@failed = false
|
|
26
|
+
@fail_error = nil
|
|
27
|
+
@listeners = {}
|
|
28
|
+
@registered_events = Set.new
|
|
29
|
+
@mutex = Mutex.new
|
|
30
|
+
@disconnect_timer = nil
|
|
31
|
+
@disconnect_generation = 0
|
|
32
|
+
@reconnect_thread = nil
|
|
33
|
+
@connect_thread = nil
|
|
34
|
+
@reconnecting = false
|
|
35
|
+
@connecting = false
|
|
36
|
+
@close_requested = false
|
|
37
|
+
@closed = false
|
|
38
|
+
@max_reconnects = 100
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Idempotent: ensure a connection attempt is in progress or already established.
|
|
42
|
+
# Non-blocking. Starts a background Thread to connect if not already connected
|
|
43
|
+
# and no attempt is currently running.
|
|
44
|
+
# @return [void]
|
|
45
|
+
def ensure_connected
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
return if @closed || @connected || @connecting || @connect_thread&.alive?
|
|
48
|
+
|
|
49
|
+
@connect_thread = Thread.new do
|
|
50
|
+
connect
|
|
51
|
+
rescue StandardError
|
|
52
|
+
# Callers check connected? when they need it
|
|
53
|
+
ensure
|
|
54
|
+
@mutex.synchronize do
|
|
55
|
+
@connect_thread = nil if @connect_thread == Thread.current
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Establish the Socket.IO connection.
|
|
62
|
+
# @return [void]
|
|
63
|
+
# @raise [StandardError] on connection failure
|
|
64
|
+
def connect
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
return if @closed || @connected || @connecting
|
|
67
|
+
|
|
68
|
+
@connecting = true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Close any existing stale connection before creating a fresh one
|
|
72
|
+
@client&.close rescue nil # rubocop:disable Style/RescueModifier
|
|
73
|
+
|
|
74
|
+
client = SocketIOClient.new(
|
|
75
|
+
api_url: @api_url,
|
|
76
|
+
token: @token,
|
|
77
|
+
organization_id: @organization_id,
|
|
78
|
+
source: @source,
|
|
79
|
+
sdk_version: @sdk_version,
|
|
80
|
+
on_event: method(:handle_event),
|
|
81
|
+
on_disconnect: method(:handle_disconnect)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
@client = client
|
|
86
|
+
@close_requested = false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
client.connect
|
|
90
|
+
|
|
91
|
+
@mutex.synchronize do
|
|
92
|
+
@connected = true
|
|
93
|
+
@failed = false
|
|
94
|
+
@fail_error = nil
|
|
95
|
+
schedule_delayed_disconnect_locked if @listeners.empty?
|
|
96
|
+
end
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
@mutex.synchronize do
|
|
99
|
+
@failed = true
|
|
100
|
+
@fail_error = "WebSocket connection failed: #{e.message}"
|
|
101
|
+
end
|
|
102
|
+
ensure
|
|
103
|
+
@mutex.synchronize { @connecting = false }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Subscribe to specific events for a resource.
|
|
107
|
+
# @param resource_id [String] The ID of the resource (e.g. sandbox ID, volume ID).
|
|
108
|
+
# @param events [Array<String>] List of Socket.IO event names to listen for.
|
|
109
|
+
# @yield [event_name, data] Called with raw event name and data hash.
|
|
110
|
+
# @return [Proc] Unsubscribe function.
|
|
111
|
+
def subscribe(resource_id, events:, &handler)
|
|
112
|
+
@mutex.synchronize do
|
|
113
|
+
# No-op after disconnect
|
|
114
|
+
return -> {} if @closed
|
|
115
|
+
|
|
116
|
+
@disconnect_timer&.kill
|
|
117
|
+
@disconnect_timer = nil
|
|
118
|
+
@disconnect_generation += 1
|
|
119
|
+
|
|
120
|
+
@listeners[resource_id] ||= []
|
|
121
|
+
@listeners[resource_id] << handler
|
|
122
|
+
# Mark events in filter under lock to avoid dropping first event
|
|
123
|
+
events.each { |evt| @registered_events.add(evt) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
ensure_connected
|
|
127
|
+
|
|
128
|
+
lambda {
|
|
129
|
+
return if @close_requested || @closed
|
|
130
|
+
|
|
131
|
+
should_schedule = false
|
|
132
|
+
@mutex.synchronize do
|
|
133
|
+
@listeners[resource_id]&.delete(handler)
|
|
134
|
+
unsubscribe_resource_locked(resource_id) if @listeners[resource_id] && @listeners[resource_id].empty?
|
|
135
|
+
should_schedule = @listeners.empty?
|
|
136
|
+
|
|
137
|
+
schedule_delayed_disconnect_locked if should_schedule
|
|
138
|
+
end
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @return [Boolean]
|
|
143
|
+
def connected?
|
|
144
|
+
@connected
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @return [Boolean]
|
|
148
|
+
def failed?
|
|
149
|
+
@failed
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @return [String, nil]
|
|
153
|
+
attr_reader :fail_error
|
|
154
|
+
|
|
155
|
+
# Disconnect and clean up.
|
|
156
|
+
def disconnect
|
|
157
|
+
thread_state = @mutex.synchronize do
|
|
158
|
+
begin_disconnect_locked(permanent: true, skip_thread: Thread.current)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
finalize_disconnect(thread_state)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def unsubscribe_resource_locked(resource_id)
|
|
167
|
+
@listeners.delete(resource_id)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def schedule_delayed_disconnect_locked
|
|
171
|
+
@disconnect_timer&.kill
|
|
172
|
+
generation = @disconnect_generation
|
|
173
|
+
@disconnect_timer = Thread.new do
|
|
174
|
+
sleep(DISCONNECT_DELAY)
|
|
175
|
+
thread_state = @mutex.synchronize do
|
|
176
|
+
next unless generation == @disconnect_generation
|
|
177
|
+
next unless @listeners.empty?
|
|
178
|
+
|
|
179
|
+
begin_disconnect_locked(permanent: false, skip_thread: Thread.current)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
finalize_disconnect(thread_state) if thread_state
|
|
183
|
+
end
|
|
184
|
+
@disconnect_timer.abort_on_exception = false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def handle_event(event_name, data)
|
|
188
|
+
# Read registered_events under lock for thread safety
|
|
189
|
+
registered = @mutex.synchronize { @registered_events.include?(event_name) }
|
|
190
|
+
return unless registered
|
|
191
|
+
|
|
192
|
+
resource_id = extract_id_from_event(data)
|
|
193
|
+
return unless resource_id
|
|
194
|
+
|
|
195
|
+
dispatch(resource_id, event_name, data)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Extract resource ID from an event payload.
|
|
199
|
+
# Handles two payload shapes:
|
|
200
|
+
# - Wrapper: {sandbox: {id: ...}, ...} -> nested resource ID
|
|
201
|
+
# - Direct: {id: ...} -> top-level ID
|
|
202
|
+
def extract_id_from_event(data)
|
|
203
|
+
return nil unless data.is_a?(Hash)
|
|
204
|
+
|
|
205
|
+
%w[sandbox volume snapshot runner].each do |key|
|
|
206
|
+
nested = data[key]
|
|
207
|
+
next unless nested.is_a?(Hash)
|
|
208
|
+
|
|
209
|
+
sid = nested['id']
|
|
210
|
+
return sid if sid.is_a?(String)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
top_id = data['id']
|
|
214
|
+
return top_id if top_id.is_a?(String)
|
|
215
|
+
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def dispatch(resource_id, event_name, data)
|
|
220
|
+
handlers = @mutex.synchronize { @listeners[resource_id]&.dup || [] }
|
|
221
|
+
handlers.each do |handler|
|
|
222
|
+
handler.call(event_name, data)
|
|
223
|
+
rescue StandardError
|
|
224
|
+
# Don't let handler errors break other handlers
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def handle_disconnect
|
|
229
|
+
@connected = false
|
|
230
|
+
return if @close_requested
|
|
231
|
+
|
|
232
|
+
@reconnect_thread = Thread.new { reconnect_loop }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def reconnect_loop
|
|
236
|
+
@mutex.synchronize do
|
|
237
|
+
return if @reconnecting
|
|
238
|
+
|
|
239
|
+
@reconnecting = true
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
attempt = 0
|
|
243
|
+
while attempt < @max_reconnects
|
|
244
|
+
if @close_requested
|
|
245
|
+
@mutex.synchronize { @reconnecting = false }
|
|
246
|
+
return
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
delay = [2**attempt, 30].min
|
|
250
|
+
sleep(delay)
|
|
251
|
+
if @close_requested
|
|
252
|
+
@mutex.synchronize { @reconnecting = false }
|
|
253
|
+
return
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
begin
|
|
257
|
+
connect
|
|
258
|
+
if @connected
|
|
259
|
+
@mutex.synchronize { @reconnecting = false }
|
|
260
|
+
return
|
|
261
|
+
end
|
|
262
|
+
rescue StandardError
|
|
263
|
+
# Continue retrying
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
attempt += 1
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# All attempts failed
|
|
270
|
+
@mutex.synchronize do
|
|
271
|
+
@failed = true
|
|
272
|
+
@fail_error = "WebSocket reconnection failed after #{@max_reconnects} attempts"
|
|
273
|
+
@reconnecting = false
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def begin_disconnect_locked(permanent:, skip_thread:)
|
|
278
|
+
@closed = true if permanent
|
|
279
|
+
@close_requested = true
|
|
280
|
+
|
|
281
|
+
thread_state = {
|
|
282
|
+
client: @client,
|
|
283
|
+
reconnect_thread: @reconnect_thread,
|
|
284
|
+
connect_thread: @connect_thread,
|
|
285
|
+
disconnect_timer: @disconnect_timer,
|
|
286
|
+
skip_thread:
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@client = nil
|
|
290
|
+
@reconnect_thread = nil
|
|
291
|
+
@connect_thread = nil
|
|
292
|
+
@disconnect_timer = nil
|
|
293
|
+
@connected = false
|
|
294
|
+
@connecting = false
|
|
295
|
+
@listeners.clear
|
|
296
|
+
@registered_events.clear
|
|
297
|
+
|
|
298
|
+
thread_state
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def finalize_disconnect(thread_state)
|
|
302
|
+
return unless thread_state
|
|
303
|
+
|
|
304
|
+
[thread_state[:reconnect_thread], thread_state[:connect_thread], thread_state[:disconnect_timer]].each do |thread|
|
|
305
|
+
next unless thread
|
|
306
|
+
next if thread == thread_state[:skip_thread]
|
|
307
|
+
|
|
308
|
+
stop_thread(thread)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
thread_state[:client]&.close
|
|
312
|
+
rescue StandardError
|
|
313
|
+
nil
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def stop_thread(thread)
|
|
317
|
+
return unless thread
|
|
318
|
+
return if thread == Thread.current
|
|
319
|
+
|
|
320
|
+
thread.kill
|
|
321
|
+
thread.join
|
|
322
|
+
rescue StandardError
|
|
323
|
+
nil
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Daytona Platforms Inc.
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'securerandom'
|
|
7
|
+
|
|
8
|
+
module Daytona
|
|
9
|
+
class EventSubscriptionManager
|
|
10
|
+
SUBSCRIPTION_TTL = 300
|
|
11
|
+
private_constant :SUBSCRIPTION_TTL
|
|
12
|
+
|
|
13
|
+
def initialize(dispatcher = nil)
|
|
14
|
+
@dispatcher = dispatcher
|
|
15
|
+
@subscriptions = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@closed = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def subscribe(resource_id:, handler:, events:)
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
return nil if @closed || @dispatcher.nil?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unsubscribe = @dispatcher.subscribe(resource_id, events:, &handler)
|
|
26
|
+
sub_id = SecureRandom.hex(16)
|
|
27
|
+
timer_to_stop = nil
|
|
28
|
+
rollback_unsubscribe = nil
|
|
29
|
+
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
if @closed
|
|
32
|
+
# Rollback dispatcher subscription on failure
|
|
33
|
+
rollback_unsubscribe = unsubscribe
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@subscriptions[sub_id] = { unsubscribe:, timer: nil }
|
|
38
|
+
timer_to_stop = start_timer_locked(sub_id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if rollback_unsubscribe
|
|
42
|
+
rollback_unsubscribe.call
|
|
43
|
+
return nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
stop_thread(timer_to_stop)
|
|
47
|
+
sub_id
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def refresh(sub_id)
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
# Reject after shutdown to prevent use-after-close
|
|
53
|
+
return false if @closed
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
timer_to_stop = nil
|
|
57
|
+
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
return false unless @subscriptions.key?(sub_id)
|
|
60
|
+
|
|
61
|
+
timer_to_stop = start_timer_locked(sub_id)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
stop_thread(timer_to_stop)
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
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
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
return unless subscription
|
|
81
|
+
|
|
82
|
+
stop_thread(timer)
|
|
83
|
+
subscription[:unsubscribe].call
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def shutdown
|
|
87
|
+
subscriptions = nil
|
|
88
|
+
|
|
89
|
+
@mutex.synchronize do
|
|
90
|
+
@closed = true
|
|
91
|
+
subscriptions = @subscriptions.values
|
|
92
|
+
@subscriptions = {}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
subscriptions.each do |subscription|
|
|
96
|
+
stop_thread(subscription[:timer])
|
|
97
|
+
subscription[:unsubscribe].call
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def start_timer_locked(sub_id)
|
|
104
|
+
subscription = @subscriptions[sub_id]
|
|
105
|
+
return unless subscription
|
|
106
|
+
|
|
107
|
+
previous_timer = cancel_timer_locked(sub_id)
|
|
108
|
+
timer = Thread.new do
|
|
109
|
+
sleep(SUBSCRIPTION_TTL)
|
|
110
|
+
|
|
111
|
+
unsubscribe = @mutex.synchronize do
|
|
112
|
+
current_subscription = @subscriptions[sub_id]
|
|
113
|
+
next unless current_subscription && current_subscription[:timer] == Thread.current
|
|
114
|
+
|
|
115
|
+
@subscriptions.delete(sub_id)&.dig(:unsubscribe)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
unsubscribe&.call
|
|
119
|
+
end
|
|
120
|
+
timer.abort_on_exception = false
|
|
121
|
+
subscription[:timer] = timer
|
|
122
|
+
previous_timer
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def cancel_timer_locked(sub_id)
|
|
126
|
+
subscription = @subscriptions[sub_id]
|
|
127
|
+
return unless subscription
|
|
128
|
+
|
|
129
|
+
timer = subscription[:timer]
|
|
130
|
+
subscription[:timer] = nil
|
|
131
|
+
timer
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def stop_thread(thread)
|
|
135
|
+
return unless thread
|
|
136
|
+
return if thread == Thread.current
|
|
137
|
+
|
|
138
|
+
thread.kill
|
|
139
|
+
thread.join
|
|
140
|
+
rescue StandardError
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|