dash 3.1.3 → 3.1.4

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: f4a83d0c755177ff76a6adbe519a705cff0c2fa32ea8a5dc4c71fbd2b10a74b3
4
- data.tar.gz: c80e2fedd3a5fb8b7e6949f384698e461b1073172ed9d99c8ccd1d7f0391d4a3
3
+ metadata.gz: 8c3e7210d642d1f0fc0d9a414cdf74a0c4a1ec9e09cf2f534c1cb1f6b02342a5
4
+ data.tar.gz: d91f21902a85f56d76bfde689a75d6c3d5b10aa587138de732943e3a0015433d
5
5
  SHA512:
6
- metadata.gz: e6ea280741d6c78b1b0ed7e6762f536dd4968fb2d9575dd347d07376cfc1128f4bbed91a2c3dc2d09a1620b8a24202385854c1ded2f74d7a6093fa8955ac7b21
7
- data.tar.gz: f3e9e2f1946fd99356a349a4d6148c4aa457ae4227ce08171f69f9811c27b8572bf8f4524f63631c533c4b01ba26ef5e4033be738fc1bd7f282a3e961d7eb4db
6
+ metadata.gz: e62792a82328365e87efd4428cfab12808281fc617157f8699e757962b742988560ff66ebdd0c5fa7f36f6071b1a46441284038c442e74724ec0b1caa354f5c9
7
+ data.tar.gz: f328a6e03c4d302170ad406a6550ddd95987622b26af74442945b25cec44081074b995a633b97f546bb873b97e3b9b3905c78973526698a5e0dd4d5635f6dd13
@@ -125,9 +125,13 @@ module Kamal::Cli
125
125
  puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
126
126
  end
127
127
 
128
- def modify(lock: false)
128
+ # Deploy lock outside, server lock inside. Every caller acquires in that
129
+ # order, and deploy locks are unique per destination, so no cycle forms.
130
+ def modify(lock: false, server_lock: false)
129
131
  KAMAL.modify(command: command, subcommand: subcommand) do
130
- lock ? with_lock { yield } : yield
132
+ guarded = -> { server_lock ? with_server_lock { yield } : yield }
133
+
134
+ lock ? with_lock(&guarded) : guarded.call
131
135
  end
132
136
  end
133
137
 
@@ -142,6 +146,95 @@ module Kamal::Cli
142
146
  raw ? KAMAL.with_verbosity(:error, &block) : block.call
143
147
  end
144
148
 
149
+ # kamal-proxy is one container per host, shared by every destination
150
+ # deployed there, but the deploy lock is per-destination — so two
151
+ # destinations deploying at once take different locks and both mutate the
152
+ # same proxy. Anything touching the proxy takes this lock as well.
153
+ def with_server_lock
154
+ if KAMAL.holding_server_lock?
155
+ yield
156
+ else
157
+ acquire_server_lock
158
+
159
+ begin
160
+ yield
161
+ ensure
162
+ release_server_lock
163
+ end
164
+ end
165
+ end
166
+
167
+ # Always waits rather than failing: the guarded work is short, and
168
+ # aborting the second destination would defeat running them in parallel.
169
+ #
170
+ # Hosts are taken one at a time and rolled back on contention. Taking them
171
+ # in one `on(hosts)` sweep would leave the locks we did win in place, and
172
+ # every retry would then collide with itself and wait out the timeout.
173
+ def acquire_server_lock
174
+ ensure_run_directory
175
+
176
+ timeout = KAMAL.lock_wait_timeout
177
+ interval = KAMAL.lock_wait_interval
178
+ deadline = Time.now + timeout
179
+ details_shown = false
180
+
181
+ say "Acquiring the server lock...", :magenta
182
+
183
+ loop do
184
+ held = []
185
+
186
+ begin
187
+ server_lock_hosts.each do |host|
188
+ execute_lock_acquire(AUTOMATIC_DEPLOY_LOCK_MESSAGE, lock: KAMAL.server_lock, hosts: host)
189
+ held << host
190
+ end
191
+
192
+ break
193
+ rescue LockHeldError
194
+ release_server_lock_on(held)
195
+
196
+ unless details_shown
197
+ say "Server lock is held by:", :magenta
198
+ puts capture_lock_status(lock: KAMAL.server_lock, hosts: server_lock_hosts.first)
199
+ details_shown = true
200
+ end
201
+
202
+ remaining = (deadline - Time.now).to_i
203
+ if remaining <= 0
204
+ say "Timed out after #{timeout}s waiting for the server lock", :red
205
+ raise LockError, "Timed out waiting for server lock"
206
+ end
207
+
208
+ say "Waiting #{interval}s for the server lock (#{remaining}s remaining)...", :magenta
209
+ sleep [ interval, remaining ].min
210
+ end
211
+ end
212
+
213
+ KAMAL.holding_server_lock = true
214
+ end
215
+
216
+ def release_server_lock
217
+ say "Releasing the server lock...", :magenta
218
+ release_server_lock_on(server_lock_hosts)
219
+
220
+ KAMAL.holding_server_lock = false
221
+ end
222
+
223
+ # Only ever called with hosts this process actually locked, so a missing
224
+ # directory means someone already cleaned up, not that we may delete
225
+ # another deploy's lock.
226
+ def release_server_lock_on(hosts)
227
+ Array(hosts).each do |host|
228
+ execute_lock_release(lock: KAMAL.server_lock, hosts: host)
229
+ rescue LockMissingError
230
+ nil
231
+ end
232
+ end
233
+
234
+ def server_lock_hosts
235
+ KAMAL.proxy_hosts.presence || KAMAL.hosts
236
+ end
237
+
145
238
  def with_lock
146
239
  if KAMAL.holding_lock?
147
240
  yield
@@ -239,23 +332,23 @@ module Kamal::Cli
239
332
  raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
240
333
  end
241
334
 
242
- def execute_lock_acquire(message)
243
- on(KAMAL.primary_host) { execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug }
335
+ def execute_lock_acquire(message, lock: KAMAL.lock, hosts: KAMAL.primary_host)
336
+ on(hosts) { execute *lock.acquire(message, KAMAL.config.version), verbosity: :debug }
244
337
  rescue SSHKit::Runner::ExecuteError => e
245
338
  raise LockHeldError if e.message =~ /cannot create directory/
246
339
  raise
247
340
  end
248
341
 
249
- def execute_lock_release
250
- on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
342
+ def execute_lock_release(lock: KAMAL.lock, hosts: KAMAL.primary_host)
343
+ on(hosts) { execute *lock.release, verbosity: :debug }
251
344
  rescue SSHKit::Runner::ExecuteError => e
252
345
  raise LockMissingError if e.message =~ /No such file or directory/
253
346
  raise
254
347
  end
255
348
 
256
- def capture_lock_status
349
+ def capture_lock_status(lock: KAMAL.lock, hosts: KAMAL.primary_host)
257
350
  status = nil
258
- on(KAMAL.primary_host) { status = capture_with_debug(*KAMAL.lock.status) }
351
+ on(hosts) { status = capture_with_debug(*lock.status) }
259
352
  status
260
353
  rescue SSHKit::Runner::ExecuteError => e
261
354
  raise LockMissingError if e.message =~ /No such file or directory/
@@ -1,7 +1,7 @@
1
1
  class Kamal::Cli::Proxy < Kamal::Cli::Base
2
2
  desc "boot", "Boot proxy on servers"
3
3
  def boot
4
- modify(lock: true) do
4
+ modify(lock: true, server_lock: true) do
5
5
  on(KAMAL.hosts) do |host|
6
6
  execute *KAMAL.docker.create_network
7
7
  rescue SSHKit::Command::Failed => e
@@ -215,7 +215,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
215
215
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
216
216
  def reboot
217
217
  confirming "This will cause a brief outage on each host. Are you sure?" do
218
- modify(lock: true) do
218
+ modify(lock: true, server_lock: true) do
219
219
  # Skip proxy on loadbalancer host - it will be handled by loadbalancer reboot
220
220
  proxy_hosts = KAMAL.proxy_hosts
221
221
  if KAMAL.config.proxy.loadbalancer_on_proxy_host?
@@ -292,7 +292,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
292
292
 
293
293
  desc "start", "Start existing proxy container on servers"
294
294
  def start
295
- modify(lock: true) do
295
+ modify(lock: true, server_lock: true) do
296
296
  on(KAMAL.proxy_hosts) do |host|
297
297
  execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
298
298
  execute *KAMAL.proxy(host).start
@@ -309,7 +309,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
309
309
 
310
310
  desc "stop", "Stop existing proxy container on servers"
311
311
  def stop
312
- modify(lock: true) do
312
+ modify(lock: true, server_lock: true) do
313
313
  on(KAMAL.proxy_hosts) do |host|
314
314
  execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
315
315
  execute *KAMAL.proxy(host).stop, raise_on_non_zero_exit: false
@@ -329,7 +329,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
329
329
 
330
330
  desc "restart", "Restart existing proxy container on servers"
331
331
  def restart
332
- modify(lock: true) do
332
+ modify(lock: true, server_lock: true) do
333
333
  stop
334
334
  start
335
335
  end
@@ -377,7 +377,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
377
377
  desc "remove", "Remove proxy container and image from servers"
378
378
  option :force, type: :boolean, default: false, desc: "Force removing proxy when apps are still installed"
379
379
  def remove
380
- modify(lock: true) do
380
+ modify(lock: true, server_lock: true) do
381
381
  if removal_allowed?(options[:force])
382
382
  stop
383
383
  remove_container
@@ -510,7 +510,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
510
510
 
511
511
  # Under the deploy lock: a concurrent deploy could boot or reboot the
512
512
  # proxy mid-export, and the offline read below would archive a torn store.
513
- modify(lock: true) do
513
+ modify(lock: true, server_lock: true) do
514
514
  on(cert_store_host) do |host|
515
515
  execute *KAMAL.auditor.record("Exported the proxy certificate store"), verbosity: :debug
516
516
 
@@ -550,7 +550,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
550
550
  force, verify = options[:force], options[:verify]
551
551
  load_balancing = KAMAL.config.proxy.load_balancing?
552
552
 
553
- modify(lock: true) do
553
+ modify(lock: true, server_lock: true) do
554
554
  on(cert_store_host) do |host|
555
555
  commands = load_balancing ? KAMAL.loadbalancer : KAMAL.proxy(host)
556
556
 
@@ -583,7 +583,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
583
583
 
584
584
  desc "remove_container", "Remove proxy container from servers", hide: true
585
585
  def remove_container
586
- modify(lock: true) do
586
+ modify(lock: true, server_lock: true) do
587
587
  on(KAMAL.proxy_hosts) do
588
588
  execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
589
589
  execute *KAMAL.proxy(host).remove_container
@@ -600,7 +600,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
600
600
 
601
601
  desc "remove_image", "Remove proxy image from servers", hide: true
602
602
  def remove_image
603
- modify(lock: true) do
603
+ modify(lock: true, server_lock: true) do
604
604
  on(KAMAL.proxy_hosts) do
605
605
  execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
606
606
  execute *KAMAL.proxy(host).remove_image
@@ -617,7 +617,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
617
617
 
618
618
  desc "remove_proxy_directory", "Remove the proxy directory from servers", hide: true
619
619
  def remove_proxy_directory
620
- modify(lock: true) do
620
+ modify(lock: true, server_lock: true) do
621
621
  on(KAMAL.proxy_hosts) do
622
622
  execute *KAMAL.proxy(host).remove_proxy_directory, raise_on_non_zero_exit: false
623
623
  end
@@ -1,7 +1,7 @@
1
1
  class Kamal::Cli::Prune < Kamal::Cli::Base
2
2
  desc "all", "Prune unused images and stopped containers"
3
3
  def all
4
- modify(lock: true) do
4
+ modify(lock: true, server_lock: true) do
5
5
  containers
6
6
  images
7
7
  end
@@ -9,7 +9,7 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
9
9
 
10
10
  desc "images", "Prune unused images"
11
11
  def images
12
- modify(lock: true) do
12
+ modify(lock: true, server_lock: true) do
13
13
  on(KAMAL.hosts) do
14
14
  execute *KAMAL.auditor.record("Pruned images"), verbosity: :debug
15
15
  execute *KAMAL.prune.dangling_images
@@ -24,7 +24,7 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
24
24
  retain = options.fetch(:retain, KAMAL.config.retain_containers)
25
25
  raise "retain must be at least 1" if retain < 1
26
26
 
27
- modify(lock: true) do
27
+ modify(lock: true, server_lock: true) do
28
28
  on(KAMAL.hosts) do |host|
29
29
  execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
30
30
 
@@ -5,7 +5,7 @@ require "active_support/broadcast_logger"
5
5
  require "active_support/notifications"
6
6
 
7
7
  class Kamal::Commander
8
- attr_accessor :verbosity, :holding_lock, :connected, :logging, :lock_wait, :lock_wait_timeout, :lock_wait_interval
8
+ attr_accessor :verbosity, :holding_lock, :holding_server_lock, :connected, :logging, :lock_wait, :lock_wait_timeout, :lock_wait_interval
9
9
  attr_reader :specific_roles, :specific_hosts
10
10
  delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :app_hosts, :proxy_hosts, :accessory_hosts, to: :specifics
11
11
 
@@ -16,6 +16,7 @@ class Kamal::Commander
16
16
  def reset
17
17
  self.verbosity = :info
18
18
  self.holding_lock = ENV["KAMAL_LOCK"] == "true"
19
+ self.holding_server_lock = ENV["KAMAL_SERVER_LOCK"] == "true"
19
20
  self.connected = false
20
21
  self.logging = false
21
22
  self.lock_wait = false
@@ -109,6 +110,12 @@ class Kamal::Commander
109
110
  @commands[:lock] ||= Kamal::Commands::Lock.new(config)
110
111
  end
111
112
 
113
+ # Guards the kamal-proxy container, which a host runs once for every
114
+ # destination deployed onto it. Every destination contends for this one.
115
+ def server_lock
116
+ @commands[:server_lock] ||= Kamal::Commands::Lock.new(config, scope: :server)
117
+ end
118
+
112
119
  def proxy(host)
113
120
  Kamal::Commands::Proxy.new(config, host: host)
114
121
  end
@@ -174,6 +181,10 @@ class Kamal::Commander
174
181
  output_logger << "#{line}\n" if logging
175
182
  end
176
183
 
184
+ def holding_server_lock?
185
+ self.holding_server_lock
186
+ end
187
+
177
188
  def holding_lock?
178
189
  self.holding_lock
179
190
  end
@@ -3,6 +3,21 @@ require "time"
3
3
  require "base64"
4
4
 
5
5
  class Kamal::Commands::Lock < Kamal::Commands::Base
6
+ # The deploy lock is scoped to service+destination, so two destinations can
7
+ # deploy at once. Server-scoped resources — above all the single kamal-proxy
8
+ # container a host runs for every destination on it — need a lock that every
9
+ # destination contends for, hence :server.
10
+ SCOPES = %i[ destination server ].freeze
11
+
12
+ attr_reader :scope
13
+
14
+ def initialize(config, scope: :destination)
15
+ raise ArgumentError, "Unknown lock scope: #{scope}" unless SCOPES.include?(scope)
16
+
17
+ super(config)
18
+ @scope = scope
19
+ end
20
+
6
21
  def acquire(message, version)
7
22
  combine \
8
23
  [ :mkdir, lock_dir ],
@@ -45,9 +60,16 @@ class Kamal::Commands::Lock < Kamal::Commands::Base
45
60
  end
46
61
 
47
62
  def lock_dir
48
- dir_name = [ "lock", config.service, config.destination ].compact.join("-")
63
+ File.join(config.run_directory, lock_dir_name)
64
+ end
49
65
 
50
- File.join(config.run_directory, dir_name)
66
+ # A server-scoped lock deliberately carries neither service nor destination:
67
+ # every deploy reaching this host must collide on the same directory.
68
+ def lock_dir_name
69
+ case scope
70
+ when :server then "lock-server"
71
+ when :destination then [ "lock", config.service, config.destination ].compact.join("-")
72
+ end
51
73
  end
52
74
 
53
75
  def lock_details_file
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "3.1.3"
2
+ VERSION = "3.1.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dash
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.3
4
+ version: 3.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson