Class: Lemans::Environments::Daytona

Inherits:
Base
  • Object
show all
Defined in:
lib/lemans/environments/daytona.rb,
lib/lemans/environments/daytona/shell.rb,
lib/lemans/environments/daytona/retries.rb,
lib/lemans/environments/daytona/sdk_tweaks.rb,
lib/lemans/environments/daytona/snapshot_store.rb

Overview

Daytona sandboxes. Daytona builds images server-side into reusable content-named snapshots and enforces the network policy itself.

Defined Under Namespace

Modules: Retries, SdkTweaks Classes: Shell, SnapshotStore

Constant Summary collapse

TTL_MINUTES =
120
DEFAULT_BUILD_TIMEOUT_SEC =
600
TRANSFER_TIMEOUT =

Workspace tarballs ride uploads/downloads, so transfers get their own budget through the SDK's streaming API instead of the global HTTP cap.

900
TRANSFER_SLOTS =

File transfers ride the SDK's typhoeus/libcurl stack, which segfaults the VM under enough concurrent easy_perform calls (a GC race on string options libcurl is still copying). Transfers are seconds each, so capping them costs little; execs and lifecycle stay fully parallel.

Concurrent::Semaphore.new(6)
CLIENT =

Trials run concurrently, and an unsynchronised memo would build several SDK clients and leak all but one.

Concurrent::Delay.new { ::Daytona::Daytona.new(credentials) }

Constants inherited from Base

Base::DEFAULT_TIMEOUT

Instance Attribute Summary collapse

Attributes inherited from Base

#build_timeout_sec, #env, #image, #labels, #network, #resources

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#exec!

Constructor Details

#initialize(image:, resources:, network:, env: {}, labels: {}, logger: nil, build_timeout_sec: nil) ⇒ Daytona

Returns a new instance of Daytona.



46
47
48
49
50
# File 'lib/lemans/environments/daytona.rb', line 46

def initialize(image:, resources:, network:, env: {}, labels: {}, logger: nil, build_timeout_sec: nil)
  super(image: image, resources: resources, network: network, env: env, labels: labels,
        build_timeout_sec: build_timeout_sec || DEFAULT_BUILD_TIMEOUT_SEC)
  @logger = logger
end

Instance Attribute Details

#sandboxObject (readonly)

Returns the value of attribute sandbox.



28
29
30
# File 'lib/lemans/environments/daytona.rb', line 28

def sandbox
  @sandbox
end

Class Method Details

.clientObject



34
# File 'lib/lemans/environments/daytona.rb', line 34

def self.client = CLIENT.value!

.credentialsObject

The CLI stores its key as DAYTONA_TOKEN, the SDK only reads DAYTONA_API_KEY; Config also resolves .env files, so go through it.

Raises:



38
39
40
41
42
43
44
# File 'lib/lemans/environments/daytona.rb', line 38

def self.credentials
  config = ::Daytona::Config.new
  config.api_key ||= config.read_env("DAYTONA_TOKEN")
  raise ConfigError, "no Daytona credentials: set DAYTONA_API_KEY or DAYTONA_TOKEN" unless config.api_key || config.jwt_token

  config
end

Instance Method Details

#download(remote_path, local_path) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
# File 'lib/lemans/environments/daytona.rb', line 81

def download(remote_path, local_path)
  local_path = Pathname(local_path)
  local_path.dirname.mkpath
  transfer do
    local_path.open("wb") do |file|
      sandbox.fs.download_file_stream(remote_path.to_s, timeout: TRANSFER_TIMEOUT) { file.write(_1) }
    end
  end
rescue *Retries::SDK_ERRORS, SystemCallError => e
  raise InfrastructureError, "daytona: could not download #{remote_path}: #{e.message}"
end

#exec(command, timeout: nil, env: {}) ⇒ Object



63
64
65
66
67
# File 'lib/lemans/environments/daytona.rb', line 63

def exec(command, timeout: nil, env: {})
  @shell.exec(command, timeout: timeout || DEFAULT_TIMEOUT, env: env)
rescue *Retries::SDK_ERRORS => e
  raise InfrastructureError, "daytona: exec failed: #{e.message}"
end

#network_policy=(policy) ⇒ Object



93
94
95
96
97
98
# File 'lib/lemans/environments/daytona.rb', line 93

def network_policy=(policy)
  sandbox.update_network_settings(**network_kwargs(policy, for_update: true))
  @network = policy
rescue *Retries::SDK_ERRORS => e
  raise InfrastructureError, "daytona: could not apply #{policy.mode} policy: #{e.message}"
end

#startObject



52
53
54
55
56
57
58
59
60
61
# File 'lib/lemans/environments/daytona.rb', line 52

def start
  @sandbox = client.create(create_params, on_snapshot_create_logs: @logger)
  @shell = Shell.new(sandbox)
  self
rescue *Retries::SDK_ERRORS => e
  # A sandbox created but never handed over would bill until its TTL:
  # the caller's ensure can only stop an environment it received.
  stop
  raise InfrastructureError, "daytona: could not start sandbox: #{e.message}"
end

#stopObject

Deleting is the only cleanup that stops the meter. Never raises: cleanup must not replace an otherwise valid result with an exception.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/lemans/environments/daytona.rb', line 102

def stop
  return if @sandbox.nil?

  id = @sandbox.id
  wait = true
  begin
    @sandbox.delete(wait:)
    @sandbox = nil
  rescue StandardError => e
    # VM shutdown: confirming destruction needs threads Ruby no longer
    # grants, but the bare DELETE needs none — the meter still stops.
    if e.is_a?(ThreadError) && wait
      wait = false
      retry
    end
    warn "lemans: sandbox #{id} may still be running — delete failed: #{e.class}: #{e.message}"
  end
end

#upload(local_path, remote_path) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/lemans/environments/daytona.rb', line 69

def upload(local_path, remote_path)
  transfer do
    # An open handle, not a path string: the SDK uploads a non-existent
    # path AS ITS OWN BYTES, so a missing file must die here as ENOENT.
    Pathname(local_path).open("rb") do |file|
      sandbox.fs.upload_file_stream(file, remote_path.to_s, timeout: TRANSFER_TIMEOUT)
    end
  end
rescue *Retries::SDK_ERRORS => e
  raise InfrastructureError, "daytona: could not upload #{local_path}: #{e.message}"
end