daytona 0.218.0 → 0.222.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8e213814357ddef2964bbb51dd2bc1639961337e6ec59e06dba8144d905aa4f
4
- data.tar.gz: a924ea5a93b4e0d0366e4758135bcd96685eb095ba2bfbd32efa414d6e6a17c0
3
+ metadata.gz: 1571cb9675a6cbd58c204ff3d703743d0acf676960cd2cc1179a60c76733b645
4
+ data.tar.gz: 1d1e628fc6216525520dd42b03ebb93a478bb91591122a2da29e54178c1e9452
5
5
  SHA512:
6
- metadata.gz: 0571d6830e6918b940119843ad602ff2c6bdb3342f84b36b4cd3d0586269e532a21721792e174e995bcdc700e2e8daae27c12f31143d669c5cd592f308631931
7
- data.tar.gz: 570d51ca03353f3b9ea61d4a988c914acf5dce042c872aa82da8243df22045211153e7431a2388c9b4ede46ec064319d2145c5c4de5626cd06518daf06a1e398
6
+ metadata.gz: 19cd6e9eaad5b31424cb9c2d4fc5298f9a8293ff11df5b4e202fcb1632a94db2d1398e2ae7717ab590ef2b333dd6e41f6d916da8b8e06560ce565f456bfd6d88
7
+ data.tar.gz: 7e2eebf97b143b321ba3cf3547e5a72e3d99367367eddc04c6456e023d88e3c1daa0f18f3a2ce513d5ff2c74d2a49a8d2bf251dae6f61ebf3e24db77979ed67e
@@ -69,6 +69,9 @@ module Daytona
69
69
  # @return [Integer, nil] Time to live in minutes (0 to disable)
70
70
  attr_accessor :ttl_minutes
71
71
 
72
+ # @return [Integer, nil] Minutes to wait for runner assignment before the API auto-destroys the sandbox
73
+ attr_accessor :queue_timeout
74
+
72
75
  # @return [Boolean, nil] Whether the Sandbox should be ephemeral
73
76
  attr_accessor :ephemeral
74
77
 
@@ -110,6 +113,7 @@ module Daytona
110
113
  # sandbox OTel data is sent to this endpoint instead of the default collector and will not be
111
114
  # available in the Daytona analytics API or dashboard.
112
115
  # @param ttl_minutes [Integer, nil] Time to live in minutes (0 to disable)
116
+ # @param queue_timeout [Integer, nil] Minutes to wait for runner assignment before the API auto-destroys the sandbox
113
117
  # @param ephemeral [Boolean, nil] Whether the Sandbox should be ephemeral
114
118
  # @param spot [Boolean, nil] GPU-only. Whether the Sandbox may be instantly terminated to free GPU
115
119
  # capacity for an on-demand GPU Sandbox
@@ -126,6 +130,7 @@ module Daytona
126
130
  auto_archive_interval: nil,
127
131
  auto_delete_interval: nil,
128
132
  ttl_minutes: nil,
133
+ queue_timeout: nil,
129
134
  volumes: nil,
130
135
  secrets: nil,
131
136
  network_block_all: nil,
@@ -149,6 +154,7 @@ module Daytona
149
154
  @auto_archive_interval = auto_archive_interval
150
155
  @auto_delete_interval = auto_delete_interval
151
156
  @ttl_minutes = ttl_minutes
157
+ @queue_timeout = queue_timeout
152
158
  @volumes = volumes
153
159
  @secrets = secrets
154
160
  @network_block_all = network_block_all
@@ -181,6 +187,7 @@ module Daytona
181
187
  auto_archive_interval:,
182
188
  auto_delete_interval:,
183
189
  ttl_minutes:,
190
+ queue_timeout:,
184
191
  volumes:,
185
192
  secrets:,
186
193
  network_block_all:,
@@ -238,6 +245,7 @@ module Daytona
238
245
  # @param auto_archive_interval [Integer, nil] Auto-archive interval in minutes
239
246
  # @param auto_delete_interval [Integer, nil] Auto-delete interval in minutes
240
247
  # @param ttl_minutes [Integer, nil] Time to live in minutes (0 to disable)
248
+ # @param queue_timeout [Integer, nil] Minutes to wait for runner assignment before the API auto-destroys the sandbox
241
249
  # @param volumes [Array<DaytonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
242
250
  # @param secrets [Hash<String, String>, nil] Organization Secrets to expose in the Sandbox, as a
243
251
  # mapping of env var name to existing Secret name
@@ -289,6 +297,7 @@ module Daytona
289
297
  # @param auto_archive_interval [Integer, nil] Auto-archive interval in minutes
290
298
  # @param auto_delete_interval [Integer, nil] Auto-delete interval in minutes
291
299
  # @param ttl_minutes [Integer, nil] Time to live in minutes (0 to disable)
300
+ # @param queue_timeout [Integer, nil] Minutes to wait for runner assignment before the API auto-destroys the sandbox
292
301
  # @param volumes [Array<DaytonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
293
302
  # @param secrets [Hash<String, String>, nil] Organization Secrets to expose in the Sandbox, as a
294
303
  # mapping of env var name to existing Secret name
@@ -65,7 +65,7 @@ module Daytona
65
65
  # @param session_id [String] Session ID of the PTY session
66
66
  # @param handle_resize [Proc, nil] Optional callback for resizing the PTY
67
67
  # @param handle_kill [Proc, nil] Optional callback for killing the PTY
68
- def initialize(websocket, session_id:, handle_resize: nil, handle_kill: nil)
68
+ def initialize(websocket, session_id:, handle_resize: nil, handle_kill: nil) # rubocop:disable Metrics/MethodLength
69
69
  @websocket = websocket
70
70
  @session_id = session_id
71
71
  @handle_resize = handle_resize
@@ -76,7 +76,9 @@ module Daytona
76
76
  @logger = Sdk.logger
77
77
 
78
78
  @status = Status::INIT
79
+ @write_mutex = Mutex.new
79
80
  subscribe
81
+ start_keepalive
80
82
  end
81
83
 
82
84
  # Check if connected to the PTY session
@@ -108,7 +110,7 @@ module Daytona
108
110
  def send_input(input)
109
111
  raise Sdk::Error, 'PTY session not connected' unless websocket.open?
110
112
 
111
- websocket.send(input)
113
+ @write_mutex.synchronize { websocket.send(input) }
112
114
  end
113
115
 
114
116
  # Resize the PTY terminal
@@ -168,10 +170,37 @@ module Daytona
168
170
  # Disconnect from the PTY session
169
171
  #
170
172
  # @return [void]
171
- def disconnect = websocket.close
173
+ def disconnect
174
+ stop_keepalive
175
+ websocket.close
176
+ end
172
177
 
173
178
  private
174
179
 
180
+ # A PTY can legitimately stay silent for a long time (a build or test run with no
181
+ # output) and intermediate proxies/load balancers drop connections that carry no
182
+ # traffic. Pings keep the connection alive without injecting input into the terminal.
183
+ #
184
+ # @return [void]
185
+ def start_keepalive
186
+ @keepalive_thread = Thread.new do
187
+ loop do
188
+ sleep(KEEPALIVE_INTERVAL)
189
+ break unless websocket.open?
190
+
191
+ @write_mutex.synchronize { websocket.send(nil, type: :ping) }
192
+ end
193
+ rescue StandardError => e
194
+ logger.debug("[Websocket] keepalive stopped: #{e.inspect}")
195
+ end
196
+ end
197
+
198
+ # @return [void]
199
+ def stop_keepalive
200
+ @keepalive_thread&.kill
201
+ @keepalive_thread = nil
202
+ end
203
+
175
204
  # @return [Symbol]
176
205
  attr_reader :status
177
206
 
@@ -206,6 +235,7 @@ module Daytona
206
235
  def on_websocket_close(error)
207
236
  logger.debug("[Websocket] close: #{error.inspect}")
208
237
  @status = Status::CLOSED
238
+ stop_keepalive
209
239
  end
210
240
 
211
241
  # @param error [WebSocket::Frame::Incoming::Client]
@@ -297,6 +327,9 @@ module Daytona
297
327
  SLEEP_INTERVAL = 0.1
298
328
  private_constant :SLEEP_INTERVAL
299
329
 
330
+ KEEPALIVE_INTERVAL = 20.0
331
+ private_constant :KEEPALIVE_INTERVAL
332
+
300
333
  module Status
301
334
  ALL = [
302
335
  INIT = 'init',
@@ -104,7 +104,7 @@ module Daytona
104
104
  # @param params [Daytona::CreateSandboxFromSnapshotParams, Daytona::CreateSandboxFromImageParams, Nil] Sandbox creation parameters
105
105
  # @return [Daytona::Sandbox] The created sandbox
106
106
  # @raise [Daytona::Sdk::Error] If auto_stop_interval, auto_pause_interval, auto_archive_interval, or ttl_minutes is negative,
107
- # or if auto_stop_interval and auto_pause_interval are both non-zero
107
+ # if queue_timeout is not a positive integer, or if auto_stop_interval and auto_pause_interval are both non-zero
108
108
  def create(params = nil, on_snapshot_create_logs: nil)
109
109
  if params.nil?
110
110
  params = CreateSandboxFromSnapshotParams.new(language: CodeLanguage::PYTHON)
@@ -249,7 +249,7 @@ module Daytona
249
249
  # @param on_snapshot_create_logs [Proc]
250
250
  # @return [Daytona::Sandbox] The created sandbox
251
251
  # @raise [Daytona::Sdk::Error] If auto_stop_interval, auto_pause_interval, auto_archive_interval, or ttl_minutes is negative,
252
- # or if auto_stop_interval and auto_pause_interval are both non-zero
252
+ # if queue_timeout is not a positive integer, or if auto_stop_interval and auto_pause_interval are both non-zero
253
253
  def _create(params, timeout: 60, on_snapshot_create_logs: nil)
254
254
  raise Sdk::Error, 'Timeout must be a non-negative number' if timeout.negative?
255
255
 
@@ -274,6 +274,10 @@ module Daytona
274
274
 
275
275
  raise Sdk::Error, 'ttl_minutes must be a non-negative integer' if params.ttl_minutes&.negative?
276
276
 
277
+ unless params.queue_timeout.nil? || (params.queue_timeout.is_a?(Integer) && params.queue_timeout >= 1)
278
+ raise Sdk::Error, 'queue_timeout must be a positive integer'
279
+ end
280
+
277
281
  labels = params.labels&.dup || {}
278
282
  labels[CODE_TOOLBOX_LANGUAGE_LABEL] = params.language.to_s if params.language
279
283
 
@@ -288,6 +292,7 @@ module Daytona
288
292
  auto_archive_interval: params.auto_archive_interval,
289
293
  auto_delete_interval: params.auto_delete_interval,
290
294
  ttl_minutes: params.ttl_minutes,
295
+ queue_timeout: params.queue_timeout,
291
296
  volumes: params.volumes,
292
297
  secrets: params.secrets&.map { |env_var, secret_name| { env_var.to_s => secret_name.to_s } },
293
298
  network_block_all: params.network_block_all,
@@ -333,13 +338,23 @@ module Daytona
333
338
  response = sandbox_api.get_sandbox(response.id)
334
339
  end
335
340
 
336
- # Get build logs URL from API
337
- build_logs_response = sandbox_api.get_build_logs_url(response.id)
338
- uri = URI.parse("#{build_logs_response.url}?follow=true")
341
+ begin
342
+ build_logs_response = sandbox_api.get_build_logs_url(response.id)
343
+ uri = URI.parse("#{build_logs_response.url}?follow=true")
344
+
345
+ headers = {}
346
+ sandbox_api.api_client.update_params_for_auth!(headers, nil, ['bearer'])
347
+ Util.stream_async(uri:, headers:, on_chunk: on_snapshot_create_logs)
348
+ rescue StandardError => e
349
+ refreshed = begin
350
+ sandbox_api.get_sandbox(response.id)
351
+ rescue StandardError
352
+ raise e
353
+ end
354
+ raise e unless destroyed_by_lifecycle?(refreshed)
339
355
 
340
- headers = {}
341
- sandbox_api.api_client.update_params_for_auth!(headers, nil, ['bearer'])
342
- Util.stream_async(uri:, headers:, on_chunk: on_snapshot_create_logs)
356
+ response = refreshed
357
+ end
343
358
  end
344
359
 
345
360
  sandbox = to_sandbox(sandbox_dto: response)
@@ -395,6 +410,15 @@ module Daytona
395
410
 
396
411
  # @param sandbox_dto [DaytonaApiClient::Sandbox, DaytonaApiClient::SandboxListItem]
397
412
  # @return [Daytona::Sandbox]
413
+ def destroyed_by_lifecycle?(sandbox_dto)
414
+ if sandbox_dto.state == DaytonaApiClient::SandboxState::DESTROYED &&
415
+ (sandbox_dto.queue_timed_out_at || sandbox_dto.spot_evicted_at)
416
+ true
417
+ else
418
+ false
419
+ end
420
+ end
421
+
398
422
  def to_sandbox(sandbox_dto:)
399
423
  Sandbox.new(
400
424
  sandbox_dto:,
@@ -116,6 +116,9 @@ module Daytona
116
116
  # @return [String, nil] When the sandbox was evicted by spot preemption
117
117
  attr_reader :spot_evicted_at
118
118
 
119
+ # @return [String, nil] When the sandbox timed out waiting in the queue for a runner
120
+ attr_reader :queue_timed_out_at
121
+
119
122
  # @return [String, nil] The GPU type assigned to the sandbox
120
123
  attr_reader :gpu_type
121
124
 
@@ -166,6 +169,9 @@ module Daytona
166
169
  # @return [String, nil] When the sandbox will be automatically destroyed (nil if no TTL is set)
167
170
  attr_reader :auto_destroy_at
168
171
 
172
+ # @return [Integer, nil] Minutes the sandbox may wait for runner assignment before the API auto-destroys it
173
+ attr_reader :queue_timeout
174
+
169
175
  # @return [String] The creation timestamp of the sandbox
170
176
  attr_reader :created_at
171
177
 
@@ -1111,6 +1117,7 @@ module Daytona
1111
1117
  @gpu = sandbox_dto.gpu
1112
1118
  @spot = sandbox_dto.spot || false
1113
1119
  @spot_evicted_at = sandbox_dto.spot_evicted_at
1120
+ @queue_timed_out_at = sandbox_dto.queue_timed_out_at
1114
1121
  @gpu_type = sandbox_dto.gpu_type
1115
1122
  @memory = sandbox_dto.memory
1116
1123
  @disk = sandbox_dto.disk
@@ -1122,6 +1129,7 @@ module Daytona
1122
1129
  @auto_archive_interval = sandbox_dto.auto_archive_interval
1123
1130
  @auto_delete_interval = sandbox_dto.auto_delete_interval
1124
1131
  @auto_destroy_at = sandbox_dto.auto_destroy_at
1132
+ @queue_timeout = sandbox_dto.queue_timeout
1125
1133
  @created_at = sandbox_dto.created_at
1126
1134
  @updated_at = sandbox_dto.updated_at
1127
1135
  @last_activity_at = sandbox_dto.last_activity_at
@@ -1245,6 +1253,7 @@ module Daytona
1245
1253
  first_poll = false
1246
1254
 
1247
1255
  safe_refresh ? refresh_data_safe : refresh
1256
+ raise_destroyed_lifecycle_error_if_needed(target_strings)
1248
1257
 
1249
1258
  mutex.synchronize do
1250
1259
  next if result_state
@@ -1270,6 +1279,8 @@ module Daytona
1270
1279
  rescue StandardError
1271
1280
  raise e
1272
1281
  end
1282
+
1283
+ raise_destroyed_lifecycle_error_if_needed(target_strings)
1273
1284
  current = state.to_s
1274
1285
  return if target_strings.include?(current)
1275
1286
 
@@ -1293,6 +1304,26 @@ module Daytona
1293
1304
  rescue StandardError # rubocop:disable Lint/SuppressedException
1294
1305
  end
1295
1306
 
1307
+ def raise_destroyed_lifecycle_error_if_needed(target_strings)
1308
+ return unless state.to_s == DaytonaApiClient::SandboxState::DESTROYED.to_s
1309
+ return if target_strings.include?(DaytonaApiClient::SandboxState::DESTROYED.to_s)
1310
+
1311
+ raise Sdk::QueueTimeoutError, queue_timeout_message if queue_timed_out_at
1312
+ return unless spot_evicted_at
1313
+
1314
+ raise Sdk::SpotEvictedError,
1315
+ "Sandbox #{id} was evicted by spot preemption at #{spot_evicted_at}"
1316
+ end
1317
+
1318
+ def queue_timeout_message
1319
+ waited = if queue_timeout.nil?
1320
+ ''
1321
+ else
1322
+ " #{queue_timeout} #{queue_timeout == 1 ? 'minute' : 'minutes'}"
1323
+ end
1324
+ "Sandbox #{id} was destroyed after waiting#{waited} for a runner (queue timed out at #{queue_timed_out_at})"
1325
+ end
1326
+
1296
1327
  def ensure_subscribed
1297
1328
  @waiter_mutex.synchronize do
1298
1329
  return unless @subscription_manager
@@ -107,6 +107,17 @@ module Daytona
107
107
  # catch the broader connection-failure category also match.
108
108
  class ConnectionTimeoutError < ConnectionError; end
109
109
 
110
+ # ---------------------------------------------------------------------
111
+ # SDK-detected sandbox lifecycle
112
+ # ---------------------------------------------------------------------
113
+
114
+ # Raised when a Sandbox was destroyed by the API because it waited longer
115
+ # than its queue timeout for a runner.
116
+ class QueueTimeoutError < TimeoutError; end
117
+
118
+ # Raised when a Sandbox was destroyed by spot preemption.
119
+ class SpotEvictedError < Error; end
120
+
110
121
  # ---------------------------------------------------------------------
111
122
  # Domain subclasses — each inherits from the HTTP-status parent that
112
123
  # matches its server-side status code.
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Daytona
7
7
  module Sdk
8
- VERSION = '0.218.0'
8
+ VERSION = '0.222.0'
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.218.0
4
+ version: 0.222.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daytona Platforms Inc.
@@ -91,42 +91,42 @@ dependencies:
91
91
  requirements:
92
92
  - - '='
93
93
  - !ruby/object:Gem::Version
94
- version: 0.218.0
94
+ version: 0.222.0
95
95
  type: :runtime
96
96
  prerelease: false
97
97
  version_requirements: !ruby/object:Gem::Requirement
98
98
  requirements:
99
99
  - - '='
100
100
  - !ruby/object:Gem::Version
101
- version: 0.218.0
101
+ version: 0.222.0
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: daytona_api_client
104
104
  requirement: !ruby/object:Gem::Requirement
105
105
  requirements:
106
106
  - - '='
107
107
  - !ruby/object:Gem::Version
108
- version: 0.218.0
108
+ version: 0.222.0
109
109
  type: :runtime
110
110
  prerelease: false
111
111
  version_requirements: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - '='
114
114
  - !ruby/object:Gem::Version
115
- version: 0.218.0
115
+ version: 0.222.0
116
116
  - !ruby/object:Gem::Dependency
117
117
  name: daytona_toolbox_api_client
118
118
  requirement: !ruby/object:Gem::Requirement
119
119
  requirements:
120
120
  - - '='
121
121
  - !ruby/object:Gem::Version
122
- version: 0.218.0
122
+ version: 0.222.0
123
123
  type: :runtime
124
124
  prerelease: false
125
125
  version_requirements: !ruby/object:Gem::Requirement
126
126
  requirements:
127
127
  - - '='
128
128
  - !ruby/object:Gem::Version
129
- version: 0.218.0
129
+ version: 0.222.0
130
130
  - !ruby/object:Gem::Dependency
131
131
  name: dotenv
132
132
  requirement: !ruby/object:Gem::Requirement