daytona 0.226.1 → 0.227.3

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: c8ac7eb78762f5ef5aceac22b8525e41f682acad86614a90a71394998abd6380
4
- data.tar.gz: 3ba18f6946d4a8145d34d2b8c7f36ad247c38aac966216b960586e9dfda8d003
3
+ metadata.gz: 1fe1f1b23586f2b803d7c8b808a8696d5148308c4dca36a02431b951f0a0be42
4
+ data.tar.gz: 7707f83ac63438f031beb4b68d0497bdbd20799dba7109bc047bbc3f56ceb116
5
5
  SHA512:
6
- metadata.gz: 6e035c3abb3b7a82d95d6720abf04e1903d8a0ae3bda6e2a04c26ff56e003be8c53432d5a11584aa30874451510c33a6aa03785d217fe3b269d0ca2c356a8d6c
7
- data.tar.gz: 2fc469d2d8a8af26d27a737a299f6ba5e1ff6e2f24b0abd4766d504a5e27396ff49846aef9833012958575676209883210f5aeb63b9c535f8e970f71910b5ffb
6
+ metadata.gz: 35c3979e512ef9d4d3657380ef20019678754849418c68b0fe1c40fcb97ad33116322c5c2fcac52a11b050d19a7e4555fb89c8b940d41459c01a8949692f0533
7
+ data.tar.gz: 4d0c59583e76f02d327c91c61c06f1e6f3941a97346fc92ebc6295f905005d045d1324c57c40932b6f311c140ad1cca2b134dbd87abfbe5c5c0991affd9909dc
data/README.md CHANGED
@@ -120,6 +120,43 @@ sandbox = daytona.create(
120
120
  )
121
121
  ```
122
122
 
123
+ ### Handle sandbox creation timeouts
124
+
125
+ There are two timeouts:
126
+
127
+ - `queue_timeout` (minutes, in the params): if no runner takes the sandbox in time, the API deletes it and `create` raises `Daytona::Sdk::QueueTimeoutError`.
128
+ - `timeout` (seconds, 60 by default, `0` for no limit): if the sandbox hasn't started in time, `create` raises `Daytona::Sdk::TimeoutError`. The sandbox may still be starting, so delete it yourself using `sandbox_id` from the error.
129
+ If the create request itself timed out, `sandbox_id` is `nil`, but the API may still have created the sandbox, and it can take a few seconds to show up. Give your sandboxes labels so you can find and delete these later with `list`.
130
+
131
+ ```ruby
132
+ require 'daytona'
133
+ require 'securerandom'
134
+
135
+ daytona = Daytona::Daytona.new
136
+ labels = { 'job' => SecureRandom.uuid }
137
+ begin
138
+ sandbox = daytona.create(
139
+ Daytona::CreateSandboxFromImageParams.new(image: Daytona::Image.debian_slim('3.12'), queue_timeout: 5, labels:),
140
+ timeout: 300
141
+ )
142
+ rescue Daytona::Sdk::TimeoutError => e
143
+ if e.sandbox_id
144
+ begin
145
+ daytona.delete(daytona.get(e.sandbox_id))
146
+ rescue Daytona::Sdk::NotFoundError
147
+ # Already gone, nothing to clean up
148
+ end
149
+ end
150
+ raise e
151
+ end
152
+ ```
153
+
154
+ A few seconds later (e.g. at the end of the job, or from a cleanup job that kept the labels), delete what a timed out create request may have left behind:
155
+
156
+ ```ruby
157
+ daytona.list(Daytona::ListSandboxesQuery.new(labels:)).each { |s| daytona.delete(s) }
158
+ ```
159
+
123
160
  ### Execute commands
124
161
 
125
162
  Execute commands in the sandbox.
@@ -102,10 +102,21 @@ module Daytona
102
102
  # Creates a sandbox with the specified parameters
103
103
  #
104
104
  # @param params [Daytona::CreateSandboxFromSnapshotParams, Daytona::CreateSandboxFromImageParams, Nil] Sandbox creation parameters
105
+ # @param timeout [Numeric] Maximum time in seconds to wait for the Sandbox to be created and started
106
+ # (defaults to 60 s). Sandbox::NO_TIMEOUT (0) means no timeout.
107
+ # @param on_snapshot_create_logs [Proc]
105
108
  # @return [Daytona::Sandbox] The created sandbox
106
- # @raise [Daytona::Sdk::Error] If auto_stop_interval, auto_pause_interval, auto_archive_interval, or ttl_minutes is negative,
107
- # if queue_timeout is not a positive integer, or if auto_stop_interval and auto_pause_interval are both non-zero
108
- def create(params = nil, on_snapshot_create_logs: nil)
109
+ # @raise [Daytona::Sdk::Error] If timeout, auto_stop_interval, auto_pause_interval, auto_archive_interval,
110
+ # or ttl_minutes is negative, if queue_timeout is not a positive integer, or if auto_stop_interval and
111
+ # auto_pause_interval are both non-zero
112
+ # @raise [Daytona::Sdk::TimeoutError] If the Sandbox is not started within timeout. When the Sandbox was
113
+ # already created, the error's sandbox_id names it: it may still be starting, so fetch it with #get
114
+ # and remove it with #delete. When the create request itself timed out, sandbox_id is nil, but the API
115
+ # may still have created the Sandbox: find it by its labels with #list (it can take a few seconds to show up)
116
+ # @raise [Daytona::Sdk::QueueTimeoutError] If the API destroyed the Sandbox because it waited longer than
117
+ # queue_timeout for a runner
118
+ # @raise [Daytona::Sdk::SpotEvictedError] If the Sandbox was destroyed by spot preemption while starting
119
+ def create(params = nil, timeout: Sandbox::DEFAULT_TIMEOUT, on_snapshot_create_logs: nil)
109
120
  if params.nil?
110
121
  params = CreateSandboxFromSnapshotParams.new(language: CodeLanguage::PYTHON)
111
122
  elsif params.language.nil?
@@ -117,7 +128,7 @@ module Daytona
117
128
  "Invalid #{CODE_TOOLBOX_LANGUAGE_LABEL}: #{params.language}. Supported languages: #{CodeLanguage::ALL.join(', ')}"
118
129
  end
119
130
 
120
- _create(params, on_snapshot_create_logs:)
131
+ _create(params, timeout:, on_snapshot_create_logs:)
121
132
  end
122
133
 
123
134
  # Deletes a Sandbox.
@@ -245,15 +256,15 @@ module Daytona
245
256
  # Creates a sandbox with the specified parameters
246
257
  #
247
258
  # @param params [Daytona::CreateSandboxFromSnapshotParams, Daytona::CreateSandboxFromImageParams] Sandbox creation parameters
248
- # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
259
+ # @param timeout [Numeric] Maximum wait time in seconds. Sandbox::NO_TIMEOUT (0) means no timeout.
249
260
  # @param on_snapshot_create_logs [Proc]
250
261
  # @return [Daytona::Sandbox] The created sandbox
251
- # @raise [Daytona::Sdk::Error] If auto_stop_interval, auto_pause_interval, auto_archive_interval, or ttl_minutes is negative,
262
+ # @raise [Daytona::Sdk::Error] If timeout, auto_stop_interval, auto_pause_interval, auto_archive_interval, or ttl_minutes is negative,
252
263
  # if queue_timeout is not a positive integer, or if auto_stop_interval and auto_pause_interval are both non-zero
253
- def _create(params, timeout: 60, on_snapshot_create_logs: nil)
264
+ def _create(params, timeout:, on_snapshot_create_logs: nil)
254
265
  raise Sdk::Error, 'Timeout must be a non-negative number' if timeout.negative?
255
266
 
256
- start_time = Time.now
267
+ start_time = monotonic_now
257
268
 
258
269
  raise Sdk::Error, 'auto_stop_interval must be a non-negative integer' if params.auto_stop_interval&.negative?
259
270
 
@@ -329,17 +340,19 @@ module Daytona
329
340
  end
330
341
  end
331
342
 
332
- response = sandbox_api.create_sandbox(create_sandbox)
343
+ response = request_sandbox_creation(create_sandbox, timeout, start_time)
344
+ sandbox_id = response.id
333
345
 
334
346
  if response.state == DaytonaApiClient::SandboxState::PENDING_BUILD && on_snapshot_create_logs
335
347
  # Wait for state to change from PENDING_BUILD before fetching logs
336
348
  while response.state == DaytonaApiClient::SandboxState::PENDING_BUILD
349
+ raise_if_timed_out(timeout, start_time)
337
350
  sleep(1)
338
- response = sandbox_api.get_sandbox(response.id)
351
+ response = sandbox_api.get_sandbox(sandbox_id)
339
352
  end
340
353
 
341
354
  begin
342
- build_logs_response = sandbox_api.get_build_logs_url(response.id)
355
+ build_logs_response = sandbox_api.get_build_logs_url(sandbox_id)
343
356
  uri = URI.parse("#{build_logs_response.url}?follow=true")
344
357
 
345
358
  headers = {}
@@ -347,7 +360,7 @@ module Daytona
347
360
  Util.stream_async(uri:, headers:, on_chunk: on_snapshot_create_logs)
348
361
  rescue StandardError => e
349
362
  refreshed = begin
350
- sandbox_api.get_sandbox(response.id)
363
+ sandbox_api.get_sandbox(sandbox_id)
351
364
  rescue StandardError
352
365
  raise e
353
366
  end
@@ -360,12 +373,14 @@ module Daytona
360
373
  sandbox = to_sandbox(sandbox_dto: response)
361
374
 
362
375
  if sandbox.state != DaytonaApiClient::SandboxState::STARTED
363
- sandbox.wait_for_sandbox_start([0.001, timeout - (Time.now - start_time)].max)
376
+ sandbox.wait_for_sandbox_start(remaining_timeout(timeout, start_time))
364
377
  end
365
378
 
366
379
  sandbox
367
380
  rescue *Sdk::API_ERROR_CLASSES => e
368
- raise Sdk.wrap_error(e, 'Failed to create sandbox')
381
+ raise name_created_sandbox(Sdk.wrap_error(e, 'Failed to create sandbox'), sandbox_id, timeout)
382
+ rescue Sdk::TimeoutError => e
383
+ raise name_created_sandbox(e, sandbox_id, timeout)
369
384
  end
370
385
 
371
386
  # @return [void]
@@ -419,6 +434,79 @@ module Daytona
419
434
  end
420
435
  end
421
436
 
437
+ # @param timeout [Numeric]
438
+ # @param start_time [Float] Reading of #monotonic_now
439
+ # @return [Numeric] What is left of timeout since start_time, at least 1 ms; Sandbox::NO_TIMEOUT stays as is
440
+ def remaining_timeout(timeout, start_time)
441
+ return Sandbox::NO_TIMEOUT if timeout.zero?
442
+
443
+ [0.001, timeout - (monotonic_now - start_time)].max
444
+ end
445
+
446
+ # @param timeout [Numeric]
447
+ # @param start_time [Float] Reading of #monotonic_now
448
+ # @return [void]
449
+ # @raise [Daytona::Sdk::TimeoutError] If timeout has run out since start_time
450
+ def raise_if_timed_out(timeout, start_time)
451
+ return if timeout.zero? || monotonic_now - start_time < timeout
452
+
453
+ raise Sdk::TimeoutError, "Sandbox build has been pending for more than #{timeout} seconds"
454
+ end
455
+
456
+ # Sends the create request with what is left of timeout as its HTTP timeout.
457
+ #
458
+ # @param create_sandbox [DaytonaApiClient::CreateSandbox]
459
+ # @param timeout [Numeric]
460
+ # @param start_time [Float] Reading of #monotonic_now
461
+ # @return [DaytonaApiClient::Sandbox]
462
+ # @raise [Daytona::Sdk::TimeoutError] If timeout runs out before or during the request
463
+ def request_sandbox_creation(create_sandbox, timeout, start_time)
464
+ message = "Failed to create and start sandbox within #{timeout} seconds. Operation timed out."
465
+ # Once timeout has run out (e.g. while uploading the build context), don't create a Sandbox
466
+ # that the TimeoutError could not name.
467
+ raise Sdk::TimeoutError, message unless timeout.zero? || monotonic_now - start_time < timeout
468
+
469
+ client = api_client_with_timeout(remaining_timeout(timeout, start_time))
470
+ DaytonaApiClient::SandboxApi.new(client).create_sandbox(create_sandbox)
471
+ rescue DaytonaApiClient::ApiError => e
472
+ # The generated client raises this, without a status code, when the request times out.
473
+ raise unless e.code.nil? && e.message == 'Connection timed out'
474
+
475
+ raise Sdk::TimeoutError, message
476
+ end
477
+
478
+ # The generated client takes a timeout only from its configuration, so this copies api_client with a
479
+ # configuration that differs only in the timeout. Headers and settings changed on api_client still apply.
480
+ #
481
+ # @param timeout [Numeric] HTTP timeout in seconds. Sandbox::NO_TIMEOUT (0) means no timeout.
482
+ # @return [DaytonaApiClient::ApiClient]
483
+ def api_client_with_timeout(timeout)
484
+ api_client.dup.tap { |client| client.config = api_client.config.dup.tap { |c| c.timeout = timeout } }
485
+ end
486
+
487
+ # @return [Float] Seconds on a clock that wall clock changes don't move, for measuring timeouts
488
+ def monotonic_now
489
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
490
+ end
491
+
492
+ # @param error [Daytona::Sdk::Error]
493
+ # @param sandbox_id [String, nil] Id of the created Sandbox, nil if creation failed
494
+ # @param timeout [Numeric]
495
+ # @return [Daytona::Sdk::Error] error as is, or a TimeoutError naming the Sandbox if it had been created
496
+ def name_created_sandbox(error, sandbox_id, timeout)
497
+ return error if sandbox_id.nil? || !error.is_a?(Sdk::TimeoutError) || error.is_a?(Sdk::QueueTimeoutError)
498
+
499
+ named = Sdk::TimeoutError.new(
500
+ "Failed to create and start sandbox within #{timeout} seconds. Operation timed out. " \
501
+ "Sandbox #{sandbox_id} was created before the timeout and may still be starting. " \
502
+ "Use daytona.get('#{sandbox_id}') to check on it, and pass the returned Sandbox to daytona.delete() " \
503
+ 'to remove it.',
504
+ status_code: error.status_code, code: error.code, source: error.source, headers: error.headers
505
+ )
506
+ named.sandbox_id = sandbox_id
507
+ named
508
+ end
509
+
422
510
  def to_sandbox(sandbox_dto:)
423
511
  Sandbox.new(
424
512
  sandbox_dto:,
@@ -18,6 +18,10 @@ module Daytona
18
18
  include Instrumentation
19
19
 
20
20
  DEFAULT_TIMEOUT = 60
21
+
22
+ # Pass as a timeout to wait without a limit.
23
+ NO_TIMEOUT = 0
24
+
21
25
  def self.with_events(*method_names)
22
26
  method_names.each do |method_name|
23
27
  original = instance_method(method_name)
@@ -1425,9 +1429,6 @@ module Daytona
1425
1429
  POLL_BACKOFF_DELAY = 5.0
1426
1430
  private_constant :POLL_BACKOFF_DELAY
1427
1431
 
1428
- NO_TIMEOUT = 0
1429
- private_constant :NO_TIMEOUT
1430
-
1431
1432
  def wait_for_snapshot_complete
1432
1433
  error_states = [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED]
1433
1434
  exclude = [DaytonaApiClient::SandboxState::SNAPSHOTTING] + error_states
@@ -74,7 +74,11 @@ module Daytona
74
74
  class NotFoundError < Error; end
75
75
 
76
76
  # HTTP 408 / 504 / client-side timeouts.
77
- class TimeoutError < Error; end
77
+ class TimeoutError < Error
78
+ # @return [String, nil] Set only on the TimeoutError from Daytona#create after the Sandbox was created:
79
+ # it may still be starting, so delete it. Nil on every other timeout.
80
+ attr_accessor :sandbox_id
81
+ end
78
82
 
79
83
  # HTTP 409 — request conflicts with current resource state.
80
84
  class ConflictError < Error; end
@@ -112,7 +116,8 @@ module Daytona
112
116
  # ---------------------------------------------------------------------
113
117
 
114
118
  # Raised when a Sandbox was destroyed by the API because it waited longer
115
- # than its queue timeout for a runner.
119
+ # than its queue timeout for a runner. Leaves sandbox_id unset, as there
120
+ # is no Sandbox left to clean up.
116
121
  class QueueTimeoutError < TimeoutError; end
117
122
 
118
123
  # Raised when a Sandbox was destroyed by spot preemption.
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Daytona
7
7
  module Sdk
8
- VERSION = '0.226.1'
8
+ VERSION = '0.227.3'
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.226.1
4
+ version: 0.227.3
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.226.1
94
+ version: 0.227.3
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.226.1
101
+ version: 0.227.3
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.226.1
108
+ version: 0.227.3
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.226.1
115
+ version: 0.227.3
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.226.1
122
+ version: 0.227.3
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.226.1
129
+ version: 0.227.3
130
130
  - !ruby/object:Gem::Dependency
131
131
  name: dotenv
132
132
  requirement: !ruby/object:Gem::Requirement