daytona 0.197.0 → 0.199.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/daytona.rb +9 -0
- 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 +39 -6
- data/lib/daytona/file_system.rb +1 -1
- data/lib/daytona/lsp_server.rb +1 -1
- data/lib/daytona/sandbox.rb +404 -111
- data/lib/daytona/sdk/version.rb +1 -1
- data/lib/daytona/sdk.rb +5 -2
- data/project.json +4 -3
- metadata +22 -5
data/lib/daytona/sandbox.rb
CHANGED
|
@@ -17,6 +17,29 @@ module Daytona
|
|
|
17
17
|
include Instrumentation
|
|
18
18
|
|
|
19
19
|
DEFAULT_TIMEOUT = 60
|
|
20
|
+
def self.with_events(*method_names)
|
|
21
|
+
method_names.each do |method_name|
|
|
22
|
+
original = instance_method(method_name)
|
|
23
|
+
|
|
24
|
+
visibility = if private_method_defined?(method_name, false)
|
|
25
|
+
:private
|
|
26
|
+
elsif protected_method_defined?(method_name, false)
|
|
27
|
+
:protected
|
|
28
|
+
else
|
|
29
|
+
:public
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
define_method(method_name) do |*args, **kwargs, &blk|
|
|
33
|
+
ensure_subscribed
|
|
34
|
+
original.bind_call(self, *args, **kwargs, &blk)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
case visibility
|
|
38
|
+
when :private then private method_name
|
|
39
|
+
when :protected then protected method_name
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
20
43
|
|
|
21
44
|
SANDBOX_METRIC_FIELD_BY_NAME = {
|
|
22
45
|
'daytona.sandbox.cpu.utilization' => :cpu_used_pct,
|
|
@@ -117,6 +140,9 @@ module Daytona
|
|
|
117
140
|
# Not returned by list results; call #refresh on each item to populate.
|
|
118
141
|
attr_reader :build_info
|
|
119
142
|
|
|
143
|
+
# @return [String, nil] When the sandbox will be automatically destroyed (nil if no TTL is set)
|
|
144
|
+
attr_reader :auto_destroy_at
|
|
145
|
+
|
|
120
146
|
# @return [String] The creation timestamp of the sandbox
|
|
121
147
|
attr_reader :created_at
|
|
122
148
|
|
|
@@ -129,6 +155,9 @@ module Daytona
|
|
|
129
155
|
# @return [String] The version of the daemon running in the sandbox
|
|
130
156
|
attr_reader :daemon_version
|
|
131
157
|
|
|
158
|
+
# @return [String] The toolbox proxy URL used to reach the sandbox's toolbox API
|
|
159
|
+
attr_reader :toolbox_proxy_url
|
|
160
|
+
|
|
132
161
|
# @return [Daytona::Config]
|
|
133
162
|
attr_reader :config
|
|
134
163
|
|
|
@@ -150,19 +179,27 @@ module Daytona
|
|
|
150
179
|
# @return [Daytona::CodeInterpreter]
|
|
151
180
|
attr_reader :code_interpreter
|
|
152
181
|
|
|
182
|
+
# Internal — obtain sandboxes via Daytona::Daytona#create, Daytona::Daytona#get,
|
|
183
|
+
# or Daytona::Daytona#list rather than constructing directly.
|
|
184
|
+
#
|
|
153
185
|
# @params config [Daytona::Config]
|
|
154
186
|
# @params sandbox_api [DaytonaApiClient::SandboxApi]
|
|
155
187
|
# @params sandbox_dto [DaytonaApiClient::Sandbox, DaytonaApiClient::SandboxListItem]
|
|
156
188
|
# @params otel_state [Daytona::OtelState, nil]
|
|
157
|
-
def initialize(sandbox_dto:, config:, sandbox_api:, otel_state: nil, analytics_api_url_provider: nil) # rubocop:disable Metrics/MethodLength
|
|
189
|
+
def initialize(sandbox_dto:, config:, sandbox_api:, subscription_manager:, otel_state: nil, analytics_api_url_provider: nil) # rubocop:disable Metrics/MethodLength
|
|
190
|
+
@state_waiters = []
|
|
191
|
+
@waiter_mutex = Mutex.new
|
|
158
192
|
process_response(sandbox_dto)
|
|
159
193
|
@config = config
|
|
160
194
|
@sandbox_api = sandbox_api
|
|
161
195
|
@otel_state = otel_state
|
|
162
196
|
@analytics_api_url_provider = analytics_api_url_provider
|
|
197
|
+
@subscription_manager = subscription_manager
|
|
198
|
+
@sub_id = nil
|
|
163
199
|
|
|
164
200
|
# Create toolbox API clients with dynamic configuration
|
|
165
|
-
toolbox_api_config = build_toolbox_api_config
|
|
201
|
+
@toolbox_api_config = build_toolbox_api_config
|
|
202
|
+
toolbox_api_config = @toolbox_api_config
|
|
166
203
|
|
|
167
204
|
# Helper to create API client with authentication header
|
|
168
205
|
create_authenticated_client = lambda do
|
|
@@ -205,6 +242,8 @@ module Daytona
|
|
|
205
242
|
@info_api = info_api
|
|
206
243
|
@server_api = server_api
|
|
207
244
|
@system_api = system_api
|
|
245
|
+
|
|
246
|
+
ensure_subscribed
|
|
208
247
|
end
|
|
209
248
|
|
|
210
249
|
# Archives the sandbox, making it inactive and preserving its state. When sandboxes are
|
|
@@ -318,20 +357,73 @@ module Daytona
|
|
|
318
357
|
@auto_pause_interval = interval
|
|
319
358
|
end
|
|
320
359
|
|
|
360
|
+
# Sets the TTL (time to live) for the Sandbox.
|
|
361
|
+
# The TTL is re-anchored from the current time. When it elapses the Sandbox is destroyed,
|
|
362
|
+
# regardless of its current state. Use 0 to disable.
|
|
363
|
+
#
|
|
364
|
+
# @param minutes [Integer]
|
|
365
|
+
# @return [void]
|
|
366
|
+
# @raise [Daytona::Sdk::Error]
|
|
367
|
+
def ttl_minutes=(minutes)
|
|
368
|
+
raise Sdk::Error, 'TTL must be a non-negative integer' if minutes.negative?
|
|
369
|
+
|
|
370
|
+
sandbox_api.set_ttl(id, minutes)
|
|
371
|
+
end
|
|
372
|
+
|
|
321
373
|
# Creates an SSH access token for the sandbox.
|
|
322
374
|
#
|
|
323
375
|
# @param expires_in_minutes [Integer] TThe number of minutes the SSH access token will be valid for
|
|
324
376
|
# @return [DaytonaApiClient::SshAccessDto]
|
|
325
|
-
def create_ssh_access(expires_in_minutes)
|
|
377
|
+
def create_ssh_access(expires_in_minutes)
|
|
378
|
+
sandbox_api.create_ssh_access(id, { expires_in_minutes: })
|
|
379
|
+
end
|
|
326
380
|
|
|
381
|
+
# Deletes the Sandbox.
|
|
382
|
+
#
|
|
383
|
+
# By default returns as soon as the deletion request is accepted (matching
|
|
384
|
+
# origin/main behavior). Pass +wait: true+ to block until the Sandbox
|
|
385
|
+
# reaches the +destroyed+ state.
|
|
386
|
+
#
|
|
387
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
388
|
+
# Only meaningful when +wait+ is true.
|
|
389
|
+
# @param wait [Boolean] When +true+, block until the Sandbox is destroyed.
|
|
327
390
|
# @return [void]
|
|
328
|
-
def delete
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
391
|
+
def delete(timeout = DEFAULT_TIMEOUT, wait: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
392
|
+
begin
|
|
393
|
+
process_response(sandbox_api.delete_sandbox(id))
|
|
394
|
+
rescue DaytonaApiClient::ApiError => e
|
|
395
|
+
raise unless e.code == 404
|
|
396
|
+
|
|
397
|
+
apply_state(DaytonaApiClient::SandboxState::DESTROYED)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
if wait
|
|
401
|
+
refresh_data_safe
|
|
402
|
+
else
|
|
403
|
+
begin
|
|
404
|
+
refresh
|
|
405
|
+
rescue DaytonaApiClient::ApiError => e
|
|
406
|
+
raise unless e.code == 404
|
|
407
|
+
|
|
408
|
+
apply_state(DaytonaApiClient::SandboxState::DESTROYED)
|
|
409
|
+
end
|
|
410
|
+
return
|
|
411
|
+
end
|
|
412
|
+
return if state.to_s == DaytonaApiClient::SandboxState::DESTROYED.to_s
|
|
333
413
|
|
|
334
|
-
|
|
414
|
+
with_timeout(
|
|
415
|
+
timeout:,
|
|
416
|
+
message: "Sandbox #{id} failed to be destroyed within the #{timeout} seconds timeout period",
|
|
417
|
+
setup: nil
|
|
418
|
+
) do
|
|
419
|
+
wait_for_state(
|
|
420
|
+
target_states: [DaytonaApiClient::SandboxState::DESTROYED],
|
|
421
|
+
error_states: [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED],
|
|
422
|
+
safe_refresh: true
|
|
423
|
+
)
|
|
424
|
+
end
|
|
425
|
+
ensure
|
|
426
|
+
unsubscribe_from_events
|
|
335
427
|
end
|
|
336
428
|
|
|
337
429
|
# Gets the user's home directory path for the logged in user inside the Sandbox.
|
|
@@ -428,7 +520,9 @@ module Daytona
|
|
|
428
520
|
#
|
|
429
521
|
# @param port [Integer]
|
|
430
522
|
# @return [DaytonaApiClient::PortPreviewUrl]
|
|
431
|
-
def preview_url(port)
|
|
523
|
+
def preview_url(port)
|
|
524
|
+
sandbox_api.get_port_preview_url(id, port)
|
|
525
|
+
end
|
|
432
526
|
|
|
433
527
|
# Creates a signed preview URL for the sandbox at the specified port.
|
|
434
528
|
#
|
|
@@ -460,7 +554,9 @@ module Daytona
|
|
|
460
554
|
# Refresh the Sandbox data from the API.
|
|
461
555
|
#
|
|
462
556
|
# @return [void]
|
|
463
|
-
def refresh
|
|
557
|
+
def refresh
|
|
558
|
+
process_response(sandbox_api.get_sandbox(id))
|
|
559
|
+
end
|
|
464
560
|
|
|
465
561
|
# Refreshes the sandbox activity to reset the timer for automated lifecycle management actions.
|
|
466
562
|
#
|
|
@@ -482,7 +578,9 @@ module Daytona
|
|
|
482
578
|
#
|
|
483
579
|
# @param token [String]
|
|
484
580
|
# @return [void]
|
|
485
|
-
def revoke_ssh_access(token)
|
|
581
|
+
def revoke_ssh_access(token)
|
|
582
|
+
sandbox_api.revoke_ssh_access(id, token:)
|
|
583
|
+
end
|
|
486
584
|
|
|
487
585
|
# Starts the Sandbox and waits for it to be ready.
|
|
488
586
|
#
|
|
@@ -493,7 +591,12 @@ module Daytona
|
|
|
493
591
|
timeout:,
|
|
494
592
|
message: "Sandbox #{id} failed to become ready within the #{timeout} seconds timeout period",
|
|
495
593
|
setup: proc { process_response(sandbox_api.start_sandbox(id)) }
|
|
496
|
-
)
|
|
594
|
+
) do
|
|
595
|
+
wait_for_state(
|
|
596
|
+
target_states: [DaytonaApiClient::SandboxState::STARTED],
|
|
597
|
+
error_states: [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED]
|
|
598
|
+
)
|
|
599
|
+
end
|
|
497
600
|
end
|
|
498
601
|
|
|
499
602
|
# Recovers the Sandbox from a recoverable error and waits for it to be ready.
|
|
@@ -510,7 +613,12 @@ module Daytona
|
|
|
510
613
|
timeout:,
|
|
511
614
|
message: "Sandbox #{id} failed to recover within the #{timeout} seconds timeout period",
|
|
512
615
|
setup: proc { process_response(sandbox_api.recover_sandbox(id)) }
|
|
513
|
-
)
|
|
616
|
+
) do
|
|
617
|
+
wait_for_state(
|
|
618
|
+
target_states: [DaytonaApiClient::SandboxState::STARTED],
|
|
619
|
+
error_states: [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED]
|
|
620
|
+
)
|
|
621
|
+
end
|
|
514
622
|
rescue StandardError => e
|
|
515
623
|
raise Sdk::Error, "Failed to recover sandbox: #{e.message}"
|
|
516
624
|
end
|
|
@@ -529,9 +637,9 @@ module Daytona
|
|
|
529
637
|
refresh
|
|
530
638
|
}
|
|
531
639
|
) do
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
640
|
+
wait_for_state(
|
|
641
|
+
target_states: [DaytonaApiClient::SandboxState::STOPPED, DaytonaApiClient::SandboxState::DESTROYED],
|
|
642
|
+
error_states: [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED]
|
|
535
643
|
)
|
|
536
644
|
end
|
|
537
645
|
end
|
|
@@ -572,7 +680,7 @@ module Daytona
|
|
|
572
680
|
resize_request = DaytonaApiClient::ResizeSandbox.new(resize_attrs)
|
|
573
681
|
process_response(sandbox_api.resize_sandbox(id, resize_request))
|
|
574
682
|
}
|
|
575
|
-
) { wait_for_resize_complete }
|
|
683
|
+
) { wait_for_resize_complete(timeout) }
|
|
576
684
|
end
|
|
577
685
|
|
|
578
686
|
# Waits for the Sandbox resize operation to complete.
|
|
@@ -580,9 +688,18 @@ module Daytona
|
|
|
580
688
|
#
|
|
581
689
|
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
|
|
582
690
|
# @return [void]
|
|
583
|
-
def wait_for_resize_complete(
|
|
584
|
-
|
|
585
|
-
|
|
691
|
+
def wait_for_resize_complete(timeout = DEFAULT_TIMEOUT)
|
|
692
|
+
with_timeout(
|
|
693
|
+
timeout:,
|
|
694
|
+
message: "Sandbox #{id} resize did not complete within the #{timeout} seconds timeout period",
|
|
695
|
+
setup: nil
|
|
696
|
+
) do
|
|
697
|
+
error_states = [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED]
|
|
698
|
+
exclude = [DaytonaApiClient::SandboxState::RESIZING] + error_states
|
|
699
|
+
target_states = DaytonaApiClient::SandboxState.all_vars.reject { |s| exclude.include?(s) }
|
|
700
|
+
|
|
701
|
+
wait_for_state(target_states:, error_states:)
|
|
702
|
+
end
|
|
586
703
|
end
|
|
587
704
|
|
|
588
705
|
# Creates a new Language Server Protocol (LSP) server instance.
|
|
@@ -601,15 +718,26 @@ module Daytona
|
|
|
601
718
|
#
|
|
602
719
|
# @param token [String]
|
|
603
720
|
# @return [DaytonaApiClient::SshAccessValidationDto]
|
|
604
|
-
def validate_ssh_access(token)
|
|
721
|
+
def validate_ssh_access(token)
|
|
722
|
+
sandbox_api.validate_ssh_access(token)
|
|
723
|
+
end
|
|
605
724
|
|
|
606
725
|
# Waits for the Sandbox to reach the 'started' state. Polls the Sandbox status until it
|
|
607
726
|
# reaches the 'started' state or encounters an error.
|
|
608
727
|
#
|
|
609
728
|
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
610
729
|
# @return [void]
|
|
611
|
-
def wait_for_sandbox_start(
|
|
612
|
-
|
|
730
|
+
def wait_for_sandbox_start(timeout = DEFAULT_TIMEOUT)
|
|
731
|
+
with_timeout(
|
|
732
|
+
timeout:,
|
|
733
|
+
message: "Sandbox #{id} failed to start within the #{timeout} seconds timeout period",
|
|
734
|
+
setup: nil
|
|
735
|
+
) do
|
|
736
|
+
wait_for_state(
|
|
737
|
+
target_states: [DaytonaApiClient::SandboxState::STARTED],
|
|
738
|
+
error_states: [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED]
|
|
739
|
+
)
|
|
740
|
+
end
|
|
613
741
|
end
|
|
614
742
|
|
|
615
743
|
# Waits for the Sandbox to reach the 'stopped' state. Polls the Sandbox status until it
|
|
@@ -618,9 +746,17 @@ module Daytona
|
|
|
618
746
|
#
|
|
619
747
|
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
620
748
|
# @return [void]
|
|
621
|
-
def wait_for_sandbox_stop(
|
|
622
|
-
|
|
623
|
-
|
|
749
|
+
def wait_for_sandbox_stop(timeout = DEFAULT_TIMEOUT)
|
|
750
|
+
with_timeout(
|
|
751
|
+
timeout:,
|
|
752
|
+
message: "Sandbox #{id} failed to stop within the #{timeout} seconds timeout period",
|
|
753
|
+
setup: nil
|
|
754
|
+
) do
|
|
755
|
+
wait_for_state(
|
|
756
|
+
target_states: [DaytonaApiClient::SandboxState::STOPPED, DaytonaApiClient::SandboxState::DESTROYED],
|
|
757
|
+
error_states: [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED]
|
|
758
|
+
)
|
|
759
|
+
end
|
|
624
760
|
end
|
|
625
761
|
|
|
626
762
|
# Forks the Sandbox, creating a new Sandbox with an identical filesystem.
|
|
@@ -643,12 +779,11 @@ module Daytona
|
|
|
643
779
|
sandbox_dto: forked_dto,
|
|
644
780
|
config:,
|
|
645
781
|
sandbox_api:,
|
|
646
|
-
|
|
782
|
+
subscription_manager: @subscription_manager,
|
|
647
783
|
otel_state:,
|
|
648
784
|
analytics_api_url_provider: @analytics_api_url_provider
|
|
649
785
|
)
|
|
650
|
-
forked.
|
|
651
|
-
target_states: [DaytonaApiClient::SandboxState::STARTED])
|
|
786
|
+
forked.wait_for_sandbox_start(timeout)
|
|
652
787
|
return forked
|
|
653
788
|
end
|
|
654
789
|
end
|
|
@@ -664,14 +799,15 @@ module Daytona
|
|
|
664
799
|
timeout:,
|
|
665
800
|
message: "Sandbox #{id} snapshot failed within the #{timeout} seconds timeout period",
|
|
666
801
|
setup: proc {
|
|
667
|
-
sandbox_api.create_sandbox_snapshot(id, DaytonaApiClient::CreateSandboxSnapshot.new(name:))
|
|
668
|
-
|
|
802
|
+
response = sandbox_api.create_sandbox_snapshot(id, DaytonaApiClient::CreateSandboxSnapshot.new(name:))
|
|
803
|
+
process_response(response)
|
|
669
804
|
}
|
|
670
805
|
) { wait_for_snapshot_complete }
|
|
671
806
|
end
|
|
672
807
|
|
|
673
808
|
# Pauses the Sandbox, freezing all running processes.
|
|
674
|
-
#
|
|
809
|
+
# Completes when the Sandbox has left the +pausing+ state (paused, stopped,
|
|
810
|
+
# archived, etc.); error states still raise.
|
|
675
811
|
#
|
|
676
812
|
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
|
|
677
813
|
# @return [void]
|
|
@@ -683,10 +819,25 @@ module Daytona
|
|
|
683
819
|
sandbox_api.pause_sandbox(id)
|
|
684
820
|
refresh
|
|
685
821
|
}
|
|
686
|
-
)
|
|
822
|
+
) do
|
|
823
|
+
error_states = [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED]
|
|
824
|
+
exclude = [DaytonaApiClient::SandboxState::PAUSING] + error_states
|
|
825
|
+
target_states = DaytonaApiClient::SandboxState.all_vars.reject { |s| exclude.include?(s) }
|
|
826
|
+
|
|
827
|
+
wait_for_state(target_states:, error_states:)
|
|
828
|
+
end
|
|
687
829
|
end
|
|
688
830
|
|
|
831
|
+
with_events :archive, :auto_archive_interval=, :auto_delete_interval=, :auto_stop_interval=,
|
|
832
|
+
:create_ssh_access, :delete, :get_user_home_dir, :get_work_dir, :labels=,
|
|
833
|
+
:preview_url, :create_signed_preview_url, :expire_signed_preview_url,
|
|
834
|
+
:refresh, :refresh_activity, :revoke_ssh_access, :start, :recover, :stop,
|
|
835
|
+
:create_lsp_server, :validate_ssh_access, :wait_for_sandbox_start,
|
|
836
|
+
:wait_for_sandbox_stop, :resize, :wait_for_resize_complete,
|
|
837
|
+
:experimental_fork, :experimental_create_snapshot, :pause
|
|
838
|
+
|
|
689
839
|
instrument :archive, :auto_archive_interval=, :auto_delete_interval=, :auto_pause_interval=, :auto_stop_interval=,
|
|
840
|
+
:ttl_minutes=,
|
|
690
841
|
:update_network_settings, :update_secrets, :update_env,
|
|
691
842
|
:create_ssh_access, :delete, :get_user_home_dir, :get_work_dir, :get_metrics, :get_metrics_latest,
|
|
692
843
|
:labels=,
|
|
@@ -782,6 +933,11 @@ module Daytona
|
|
|
782
933
|
# @return [DaytonaToolboxApiClient::Configuration]
|
|
783
934
|
def build_toolbox_api_config
|
|
784
935
|
DaytonaToolboxApiClient::Configuration.new.configure do |cfg|
|
|
936
|
+
if @toolbox_proxy_url.nil? || @toolbox_proxy_url.empty?
|
|
937
|
+
proxy_response = @sandbox_api.get_toolbox_proxy_url(id)
|
|
938
|
+
@toolbox_proxy_url = proxy_response&.url || ''
|
|
939
|
+
end
|
|
940
|
+
|
|
785
941
|
proxy_url = @toolbox_proxy_url
|
|
786
942
|
proxy_url += '/' unless proxy_url.end_with?('/')
|
|
787
943
|
full_url = "#{proxy_url}#{id}"
|
|
@@ -795,9 +951,26 @@ module Daytona
|
|
|
795
951
|
end
|
|
796
952
|
end
|
|
797
953
|
|
|
954
|
+
def update_toolbox_base_url
|
|
955
|
+
return unless @toolbox_api_config && @toolbox_proxy_url
|
|
956
|
+
|
|
957
|
+
proxy_url = @toolbox_proxy_url
|
|
958
|
+
proxy_url += '/' unless proxy_url.end_with?('/')
|
|
959
|
+
full_url = "#{proxy_url}#{id}"
|
|
960
|
+
uri = URI(full_url)
|
|
961
|
+
|
|
962
|
+
@toolbox_api_config.scheme = uri.scheme
|
|
963
|
+
@toolbox_api_config.host = uri.authority
|
|
964
|
+
@toolbox_api_config.base_path = uri.path.empty? ? '/' : uri.path
|
|
965
|
+
end
|
|
966
|
+
|
|
798
967
|
# @params sandbox_dto [DaytonaApiClient::Sandbox, DaytonaApiClient::SandboxListItem]
|
|
799
968
|
# @return [void]
|
|
800
969
|
def process_response(sandbox_dto) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
970
|
+
return if sandbox_dto.nil?
|
|
971
|
+
|
|
972
|
+
new_state = sandbox_dto.state
|
|
973
|
+
|
|
801
974
|
# Fields shared by both DaytonaApiClient::Sandbox and DaytonaApiClient::SandboxListItem.
|
|
802
975
|
@id = sandbox_dto.id
|
|
803
976
|
@organization_id = sandbox_dto.organization_id
|
|
@@ -810,7 +983,6 @@ module Daytona
|
|
|
810
983
|
@gpu = sandbox_dto.gpu
|
|
811
984
|
@memory = sandbox_dto.memory
|
|
812
985
|
@disk = sandbox_dto.disk
|
|
813
|
-
@state = sandbox_dto.state
|
|
814
986
|
@desired_state = sandbox_dto.desired_state
|
|
815
987
|
@error_reason = sandbox_dto.error_reason
|
|
816
988
|
@backup_state = sandbox_dto.backup_state
|
|
@@ -818,23 +990,43 @@ module Daytona
|
|
|
818
990
|
@auto_pause_interval = sandbox_dto.auto_pause_interval
|
|
819
991
|
@auto_archive_interval = sandbox_dto.auto_archive_interval
|
|
820
992
|
@auto_delete_interval = sandbox_dto.auto_delete_interval
|
|
993
|
+
@auto_destroy_at = sandbox_dto.auto_destroy_at
|
|
821
994
|
@created_at = sandbox_dto.created_at
|
|
822
995
|
@updated_at = sandbox_dto.updated_at
|
|
823
996
|
@last_activity_at = sandbox_dto.last_activity_at
|
|
824
997
|
@daemon_version = sandbox_dto.daemon_version
|
|
825
|
-
|
|
998
|
+
|
|
999
|
+
new_proxy_url = sandbox_dto.toolbox_proxy_url
|
|
1000
|
+
if new_proxy_url && !new_proxy_url.empty? && new_proxy_url != @toolbox_proxy_url
|
|
1001
|
+
@toolbox_proxy_url = new_proxy_url
|
|
1002
|
+
update_toolbox_base_url
|
|
1003
|
+
end
|
|
826
1004
|
|
|
827
1005
|
# Fields only present on the full DaytonaApiClient::Sandbox DTO (not returned by list
|
|
828
1006
|
# results; call #refresh on each item to populate them).
|
|
829
|
-
|
|
1007
|
+
if sandbox_dto.is_a?(DaytonaApiClient::Sandbox)
|
|
1008
|
+
@env = sandbox_dto.env
|
|
1009
|
+
@network_block_all = sandbox_dto.network_block_all
|
|
1010
|
+
@network_allow_list = sandbox_dto.network_allow_list
|
|
1011
|
+
@domain_allow_list = sandbox_dto.domain_allow_list
|
|
1012
|
+
@volumes = sandbox_dto.volumes
|
|
1013
|
+
@build_info = sandbox_dto.build_info
|
|
1014
|
+
@backup_created_at = sandbox_dto.backup_created_at
|
|
1015
|
+
end
|
|
830
1016
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
@
|
|
1017
|
+
apply_state(new_state)
|
|
1018
|
+
end
|
|
1019
|
+
|
|
1020
|
+
def apply_state(new_state)
|
|
1021
|
+
return if new_state.to_s == @state.to_s
|
|
1022
|
+
|
|
1023
|
+
@state = new_state
|
|
1024
|
+
|
|
1025
|
+
@waiter_mutex.synchronize do
|
|
1026
|
+
@state_waiters.each do |waiter|
|
|
1027
|
+
waiter[:notify].call(new_state.to_s) if waiter[:matches].call(new_state.to_s)
|
|
1028
|
+
end
|
|
1029
|
+
end
|
|
838
1030
|
end
|
|
839
1031
|
|
|
840
1032
|
# Monitors block not to exceed max execution time.
|
|
@@ -848,99 +1040,200 @@ module Daytona
|
|
|
848
1040
|
start_at = Time.now
|
|
849
1041
|
setup&.call
|
|
850
1042
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1043
|
+
effective = if setup
|
|
1044
|
+
remaining = timeout - (Time.now - start_at)
|
|
1045
|
+
raise Sdk::TimeoutError, message if timeout.positive? && remaining <= 0
|
|
1046
|
+
|
|
1047
|
+
[NO_TIMEOUT, remaining].max
|
|
1048
|
+
else
|
|
1049
|
+
timeout
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
Timeout.timeout(effective, Sdk::TimeoutError, message, &)
|
|
857
1053
|
end
|
|
858
1054
|
|
|
859
|
-
# Waits for the Sandbox to reach
|
|
860
|
-
#
|
|
861
|
-
# for the Sandbox to reach one of the target states.
|
|
1055
|
+
# Waits for the Sandbox to reach one of the target states via WebSocket events
|
|
1056
|
+
# with periodic polling safety net.
|
|
862
1057
|
#
|
|
863
|
-
# @param
|
|
864
|
-
# @param
|
|
1058
|
+
# @param target_states [Array<DaytonaApiClient::SandboxState>] States that indicate success.
|
|
1059
|
+
# @param error_states [Array<DaytonaApiClient::SandboxState>] States that indicate failure.
|
|
1060
|
+
# @param safe_refresh [Boolean] If true, use refresh_data_safe for polling (swallows transient errors).
|
|
865
1061
|
# @return [void]
|
|
866
1062
|
# @raise [Daytona::Sdk::Error]
|
|
867
|
-
def
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1063
|
+
def wait_for_state(target_states:, error_states:, safe_refresh: false) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
1064
|
+
ensure_subscribed
|
|
1065
|
+
subscribed = !@sub_id.nil?
|
|
1066
|
+
|
|
1067
|
+
target_strings = target_states.map(&:to_s)
|
|
1068
|
+
error_strings = error_states.map(&:to_s)
|
|
1069
|
+
started_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
1070
|
+
poll_interval = subscribed ? MAX_POLL_INTERVAL : INITIAL_POLL_INTERVAL
|
|
1071
|
+
|
|
1072
|
+
mutex = Mutex.new
|
|
1073
|
+
state_changed = ConditionVariable.new
|
|
1074
|
+
result_state = nil
|
|
1075
|
+
|
|
1076
|
+
waiter = {
|
|
1077
|
+
matches: lambda do |new_state|
|
|
1078
|
+
target_strings.include?(new_state) || error_strings.include?(new_state)
|
|
1079
|
+
end,
|
|
1080
|
+
notify: lambda do |new_state|
|
|
1081
|
+
mutex.synchronize do
|
|
1082
|
+
next if result_state
|
|
1083
|
+
|
|
1084
|
+
result_state = new_state
|
|
1085
|
+
state_changed.signal
|
|
1086
|
+
end
|
|
1087
|
+
end
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
@waiter_mutex.synchronize do
|
|
1091
|
+
@state_waiters << waiter
|
|
1092
|
+
|
|
1093
|
+
current_state = state.to_s
|
|
1094
|
+
result_state = current_state if target_strings.include?(current_state)
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
begin
|
|
1098
|
+
first_poll = true
|
|
1099
|
+
until mutex.synchronize { result_state }
|
|
1100
|
+
unless first_poll
|
|
1101
|
+
should_refresh = mutex.synchronize do
|
|
1102
|
+
next false if result_state
|
|
1103
|
+
|
|
1104
|
+
state_changed.wait(mutex, poll_interval)
|
|
1105
|
+
result_state.nil?
|
|
1106
|
+
end
|
|
1107
|
+
next unless should_refresh
|
|
1108
|
+
end
|
|
1109
|
+
first_poll = false
|
|
1110
|
+
|
|
1111
|
+
safe_refresh ? refresh_data_safe : refresh
|
|
1112
|
+
|
|
1113
|
+
mutex.synchronize do
|
|
1114
|
+
next if result_state
|
|
1115
|
+
|
|
1116
|
+
current = state.to_s
|
|
1117
|
+
result_state = current if target_strings.include?(current) || error_strings.include?(current)
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
poll_interval = next_wait_for_state_poll_interval(
|
|
1121
|
+
current_interval: poll_interval,
|
|
1122
|
+
started_at: started_at,
|
|
1123
|
+
subscribed: subscribed
|
|
1124
|
+
)
|
|
875
1125
|
end
|
|
876
1126
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1127
|
+
if result_state && error_strings.include?(result_state)
|
|
1128
|
+
raise Sdk::Error,
|
|
1129
|
+
"Sandbox #{id} entered error state: #{result_state}, error reason: #{error_reason}"
|
|
1130
|
+
end
|
|
1131
|
+
rescue Sdk::TimeoutError => e
|
|
1132
|
+
begin
|
|
1133
|
+
safe_refresh ? refresh_data_safe : refresh
|
|
1134
|
+
rescue StandardError
|
|
1135
|
+
raise e
|
|
1136
|
+
end
|
|
1137
|
+
current = state.to_s
|
|
1138
|
+
return if target_strings.include?(current)
|
|
1139
|
+
|
|
1140
|
+
if error_strings.include?(current)
|
|
1141
|
+
raise Sdk::Error,
|
|
1142
|
+
"Sandbox #{id} entered error state: #{current}, error reason: #{error_reason}"
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
raise e
|
|
1146
|
+
ensure
|
|
1147
|
+
@waiter_mutex.synchronize do
|
|
1148
|
+
@state_waiters.delete(waiter)
|
|
880
1149
|
end
|
|
881
|
-
refresh
|
|
882
1150
|
end
|
|
883
1151
|
end
|
|
884
1152
|
|
|
885
|
-
|
|
886
|
-
|
|
1153
|
+
def refresh_data_safe
|
|
1154
|
+
refresh
|
|
1155
|
+
rescue DaytonaApiClient::ApiError => e
|
|
1156
|
+
apply_state(DaytonaApiClient::SandboxState::DESTROYED) if e.code == 404
|
|
1157
|
+
rescue StandardError # rubocop:disable Lint/SuppressedException
|
|
1158
|
+
end
|
|
887
1159
|
|
|
888
|
-
|
|
889
|
-
|
|
1160
|
+
def ensure_subscribed
|
|
1161
|
+
@waiter_mutex.synchronize do
|
|
1162
|
+
return unless @subscription_manager
|
|
890
1163
|
|
|
891
|
-
|
|
892
|
-
|
|
1164
|
+
if @sub_id
|
|
1165
|
+
return if @subscription_manager.refresh(@sub_id)
|
|
893
1166
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
OPERATION_START = :start
|
|
898
|
-
private_constant :OPERATION_START
|
|
1167
|
+
@sub_id = nil
|
|
1168
|
+
end
|
|
899
1169
|
|
|
900
|
-
|
|
901
|
-
|
|
1170
|
+
subscribe_to_events
|
|
1171
|
+
end
|
|
1172
|
+
end
|
|
902
1173
|
|
|
903
|
-
|
|
904
|
-
|
|
1174
|
+
def subscribe_to_events
|
|
1175
|
+
return if @sub_id || !@subscription_manager
|
|
905
1176
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1177
|
+
@sub_id = @subscription_manager.subscribe(
|
|
1178
|
+
resource_id: id,
|
|
1179
|
+
handler: method(:handle_event),
|
|
1180
|
+
events: ['sandbox.state.updated', 'sandbox.created']
|
|
1181
|
+
)
|
|
1182
|
+
end
|
|
911
1183
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
"Sandbox #{id} snapshot failed with state: #{state}, error reason: #{error_reason}"
|
|
915
|
-
end
|
|
1184
|
+
def handle_event(event_name, data)
|
|
1185
|
+
return unless data.is_a?(Hash)
|
|
916
1186
|
|
|
917
|
-
|
|
1187
|
+
raw = data['sandbox'] || data
|
|
1188
|
+
return unless raw.is_a?(Hash)
|
|
918
1189
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1190
|
+
if event_name == 'sandbox.created'
|
|
1191
|
+
process_response(DaytonaApiClient::Sandbox.build_from_hash(raw))
|
|
1192
|
+
else
|
|
1193
|
+
new_state = raw['state'] || data['newState']
|
|
1194
|
+
apply_state(new_state) if new_state
|
|
923
1195
|
end
|
|
1196
|
+
rescue StandardError
|
|
1197
|
+
nil
|
|
924
1198
|
end
|
|
925
1199
|
|
|
926
|
-
def
|
|
927
|
-
|
|
928
|
-
start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
929
|
-
while state == DaytonaApiClient::SandboxState::PAUSING
|
|
930
|
-
refresh
|
|
1200
|
+
def unsubscribe_from_events
|
|
1201
|
+
return unless @sub_id
|
|
931
1202
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
end
|
|
1203
|
+
@subscription_manager&.unsubscribe(@sub_id)
|
|
1204
|
+
@sub_id = nil
|
|
1205
|
+
end
|
|
936
1206
|
|
|
937
|
-
|
|
1207
|
+
def next_wait_for_state_poll_interval(current_interval:, started_at:, subscribed:)
|
|
1208
|
+
return [current_interval * BACKOFF_MULTIPLIER, MAX_POLL_INTERVAL].min if subscribed
|
|
938
1209
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1210
|
+
elapsed = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - started_at
|
|
1211
|
+
return current_interval if elapsed <= POLL_BACKOFF_DELAY
|
|
1212
|
+
|
|
1213
|
+
[current_interval * BACKOFF_MULTIPLIER, MAX_POLL_INTERVAL].min
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
INITIAL_POLL_INTERVAL = 0.1
|
|
1217
|
+
private_constant :INITIAL_POLL_INTERVAL
|
|
1218
|
+
|
|
1219
|
+
MAX_POLL_INTERVAL = 1.0
|
|
1220
|
+
private_constant :MAX_POLL_INTERVAL
|
|
1221
|
+
|
|
1222
|
+
BACKOFF_MULTIPLIER = 1.1
|
|
1223
|
+
private_constant :BACKOFF_MULTIPLIER
|
|
1224
|
+
|
|
1225
|
+
POLL_BACKOFF_DELAY = 5.0
|
|
1226
|
+
private_constant :POLL_BACKOFF_DELAY
|
|
1227
|
+
|
|
1228
|
+
NO_TIMEOUT = 0
|
|
1229
|
+
private_constant :NO_TIMEOUT
|
|
1230
|
+
|
|
1231
|
+
def wait_for_snapshot_complete
|
|
1232
|
+
error_states = [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED]
|
|
1233
|
+
exclude = [DaytonaApiClient::SandboxState::SNAPSHOTTING] + error_states
|
|
1234
|
+
target_states = DaytonaApiClient::SandboxState.all_vars.reject { |s| exclude.include?(s) }
|
|
1235
|
+
|
|
1236
|
+
wait_for_state(target_states:, error_states:)
|
|
944
1237
|
end
|
|
945
1238
|
end
|
|
946
1239
|
end
|