daytona 0.227.0 → 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 +4 -4
- data/README.md +37 -0
- data/lib/daytona/daytona.rb +102 -14
- data/lib/daytona/sandbox.rb +4 -3
- data/lib/daytona/sdk/errors.rb +7 -2
- data/lib/daytona/sdk/version.rb +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1fe1f1b23586f2b803d7c8b808a8696d5148308c4dca36a02431b951f0a0be42
|
|
4
|
+
data.tar.gz: 7707f83ac63438f031beb4b68d0497bdbd20799dba7109bc047bbc3f56ceb116
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
data/lib/daytona/daytona.rb
CHANGED
|
@@ -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,
|
|
107
|
-
# if queue_timeout is not a positive integer, or if auto_stop_interval and
|
|
108
|
-
|
|
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 (
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:,
|
data/lib/daytona/sandbox.rb
CHANGED
|
@@ -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
|
data/lib/daytona/sdk/errors.rb
CHANGED
|
@@ -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
|
|
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.
|
data/lib/daytona/sdk/version.rb
CHANGED
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.227.
|
|
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.227.
|
|
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.227.
|
|
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.227.
|
|
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.227.
|
|
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.227.
|
|
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.227.
|
|
129
|
+
version: 0.227.3
|
|
130
130
|
- !ruby/object:Gem::Dependency
|
|
131
131
|
name: dotenv
|
|
132
132
|
requirement: !ruby/object:Gem::Requirement
|