dash 4.0.2 → 4.0.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: d2fca4cca966b5e857e1d537799367c1493701a1e18e48a691c53dddac6b2c36
4
- data.tar.gz: 2627d3e285ddbee1da9b4bc0705c38fb3825345f554b3b7e8f85598810c0b354
3
+ metadata.gz: 762f586250ded9a4e4f60850a91b1358bd993bcb1d4c6e291dfec93dc6d516e2
4
+ data.tar.gz: d87d3161396bfdb38f9feb3d3946d0830bdd2b1dc2921e569a275e65f4285cb1
5
5
  SHA512:
6
- metadata.gz: d40d1805b3f3b1ed9f0a79bf53a6d96709ea7c3693cc0d8319791e92b10c345aaa2743ef8fdbc568cf9b79de5b37659fb2fbbb75b7d4fac0f111322bc2f807c2
7
- data.tar.gz: 2972113fc39cd3dcbdd6476d91634e29f757989ca4c84e9dbd1dac6443f6169b967a3f3c9b9c5d7ed9913d212e18d8239c1cd142981a7a14f90dea56769fca1b
6
+ metadata.gz: 86a1e05606121be7d85b83c2da117d8bcc2a2badbf080a5e5d0237b3175dd98ac9abba3d1afa154495d6be85923d8ef7d915e3bb877241b69a637ab47de00658
7
+ data.tar.gz: 9bca1f4085e3ad18ca778eba0dc3344b6592404839fdb4628a2a1bc05d81d869c4b4a837a18f66659f2c0b9fba307da4f14d877f0c088f709d72806a68bf2927
data/lib/dash/cli/base.rb CHANGED
@@ -217,7 +217,7 @@ module Dash::Cli
217
217
 
218
218
  break
219
219
  rescue LockHeldError
220
- release_server_lock_on(held)
220
+ roll_back_server_lock(held)
221
221
 
222
222
  unless details_shown
223
223
  # The holder can release between our failed mkdir and this read.
@@ -248,12 +248,28 @@ module Dash::Cli
248
248
 
249
249
  say "Waiting #{interval}s for the server lock (#{remaining}s remaining)...", :magenta
250
250
  sleep [ interval, remaining ].min
251
+ rescue StandardError
252
+ # Anything else - a dropped SSH connection on a host that idled
253
+ # through the wait, a full disk - leaves the hosts we did take
254
+ # locked. holding_server_lock? is still false, so with_server_lock's
255
+ # ensure never runs and nothing else would ever release them.
256
+ roll_back_server_lock(held)
257
+ raise
251
258
  end
252
259
  end
253
260
 
254
261
  DASH.holding_server_lock = true
255
262
  end
256
263
 
264
+ # Never raises: on the contention path a raise here would abandon the
265
+ # locks it was rolling back, and on the failure path it would replace the
266
+ # error the operator needs to see.
267
+ def roll_back_server_lock(held)
268
+ release_server_lock_on(held)
269
+ rescue StandardError => e
270
+ say "Error releasing the server lock on #{Array(held).join(", ")}: #{e.message}", :red
271
+ end
272
+
257
273
  def release_server_lock
258
274
  say "Releasing the server lock...", :magenta
259
275
  release_server_lock_on(server_lock_hosts)
@@ -264,12 +280,22 @@ module Dash::Cli
264
280
  # Only ever called with hosts this process actually locked, so a missing
265
281
  # directory means someone already cleaned up, not that we may delete
266
282
  # another deploy's lock.
283
+ #
284
+ # Every host is attempted even after one fails - stopping at the first
285
+ # error would strand the remaining locks with no one left to release them
286
+ # - and the first error is re-raised once the sweep is done.
267
287
  def release_server_lock_on(hosts)
288
+ error = nil
289
+
268
290
  Array(hosts).each do |host|
269
291
  execute_lock_release(lock: DASH.server_lock, hosts: host)
270
292
  rescue LockMissingError
271
293
  nil
294
+ rescue StandardError => e
295
+ error ||= e
272
296
  end
297
+
298
+ raise error if error
273
299
  end
274
300
 
275
301
  def server_lock_hosts
data/lib/dash/cli/lock.rb CHANGED
@@ -1,8 +1,13 @@
1
1
  class Dash::Cli::Lock < Dash::Cli::Base
2
2
  desc "status", "Report lock status"
3
+ option :server, type: :boolean, default: false, desc: "Report the shared server lock instead of the deploy lock"
3
4
  def status
4
- handle_missing_lock do
5
- puts capture_lock_status
5
+ if options[:server]
6
+ report_server_lock_status
7
+ else
8
+ handle_missing_lock do
9
+ puts capture_lock_status
10
+ end
6
11
  end
7
12
  end
8
13
 
@@ -18,10 +23,16 @@ class Dash::Cli::Lock < Dash::Cli::Base
18
23
  end
19
24
 
20
25
  desc "release", "Release the deploy lock"
26
+ option :server, type: :boolean, default: false, desc: "Release the shared server lock instead of the deploy lock"
21
27
  def release
22
- handle_missing_lock do
23
- execute_lock_release
24
- say "Released the deploy lock"
28
+ if options[:server]
29
+ release_server_lock_on(server_lock_hosts)
30
+ say "Released the server lock"
31
+ else
32
+ handle_missing_lock do
33
+ execute_lock_release
34
+ say "Released the deploy lock"
35
+ end
25
36
  end
26
37
  end
27
38
 
@@ -31,4 +42,23 @@ class Dash::Cli::Lock < Dash::Cli::Base
31
42
  rescue LockMissingError
32
43
  say "There is no deploy lock"
33
44
  end
45
+
46
+ # The server lock is taken per host rather than per destination, so report
47
+ # each one: an acquire that failed part-way leaves it held on a subset.
48
+ def report_server_lock_status
49
+ server_lock_hosts.each do |host|
50
+ if (status = server_lock_status_on(host))
51
+ say "Server lock on #{host}:"
52
+ puts status
53
+ else
54
+ say "There is no server lock on #{host}"
55
+ end
56
+ end
57
+ end
58
+
59
+ def server_lock_status_on(host)
60
+ capture_lock_status(lock: DASH.server_lock, hosts: host)
61
+ rescue LockMissingError
62
+ nil
63
+ end
34
64
  end
@@ -19,6 +19,7 @@ class Dash::Cli::Proxy::LoadbalancerReboot
19
19
  def run
20
20
  execute *DASH.auditor.record("Rebooted loadbalancer"), verbosity: :debug
21
21
  execute *DASH.registry.login
22
+ ensure_network
22
23
 
23
24
  info "Stopping and removing #{DASH.loadbalancer.container_name} on #{host}, if running..."
24
25
  execute *DASH.loadbalancer.stop, raise_on_non_zero_exit: false
@@ -45,6 +46,16 @@ class Dash::Cli::Proxy::LoadbalancerReboot
45
46
  end
46
47
 
47
48
  private
49
+ # Before the old container is stopped: `docker run` against a missing
50
+ # network would otherwise fail with the edge already down. A dedicated
51
+ # loadbalancer host has no other path that creates the network
52
+ # (zoolutions/dash#140).
53
+ def ensure_network
54
+ execute *DASH.docker.create_network
55
+ rescue SSHKit::Command::Failed => e
56
+ raise unless e.message.include?("already exists")
57
+ end
58
+
48
59
  def wait_until_ready
49
60
  deadline = Time.now + READY_TIMEOUT
50
61
 
@@ -2,7 +2,7 @@ class Dash::Cli::Proxy < Dash::Cli::Base
2
2
  desc "boot", "Boot proxy on servers"
3
3
  def boot
4
4
  modify(lock: true, server_lock: true) do
5
- on(DASH.hosts) do |host|
5
+ on(network_hosts) do |host|
6
6
  execute *DASH.docker.create_network
7
7
  rescue SSHKit::Command::Failed => e
8
8
  raise unless e.message.include?("already exists")
@@ -76,8 +76,10 @@ class Dash::Cli::Proxy < Dash::Cli::Base
76
76
  info "Starting loadbalancer on #{host}..."
77
77
  execute *DASH.registry.login
78
78
 
79
- # Bring a pre-rename volume across before the container can be created
80
- # against an empty one. A no-op once it has been.
79
+ # Bring a pre-rename host across before the container can be created
80
+ # against an empty volume or a network nothing else joined. A no-op
81
+ # once it has been.
82
+ execute *DASH.docker.connect_legacy_network_containers
81
83
  execute *DASH.loadbalancer.copy_legacy_config_volume
82
84
 
83
85
  # The load balancer terminates TLS and owns the cache, so its host
@@ -653,6 +655,15 @@ class Dash::Cli::Proxy < Dash::Cli::Base
653
655
  end
654
656
 
655
657
  private
658
+ # Every host that runs a container on the proxy network. A dedicated
659
+ # loadbalancer host is not in DASH.hosts, and nothing else creates the
660
+ # network there (zoolutions/dash#140).
661
+ def network_hosts
662
+ hosts = DASH.hosts
663
+ hosts |= [ DASH.config.proxy.effective_loadbalancer ] if DASH.config.proxy.load_balancing?
664
+ hosts
665
+ end
666
+
656
667
  # The host that owns TLS, and so the certificate store: the loadbalancer
657
668
  # host when load balancing (TLS terminates at the edge), else the primary
658
669
  # host - the same host `loadbalancer: true` would resolve to.
@@ -20,6 +20,10 @@ sshkit:
20
20
  #
21
21
  # Kamal sets a long idle timeout of 900 seconds on connections to try to avoid
22
22
  # re-connection storms after an idle period, such as building an image or waiting for CI.
23
+ #
24
+ # A pooled connection that the network dropped while idle is evicted and the command
25
+ # retried once on a fresh connection, so a long wait elsewhere (a server-lock poll, a
26
+ # loadbalancer reboot) does not fail the next command on another host.
23
27
  pool_idle_timeout: 300
24
28
 
25
29
  # DNS retry settings
@@ -170,6 +170,50 @@ class SSHKit::Backend::Netssh
170
170
  end
171
171
  end
172
172
  prepend LimitConcurrentStartsInstance
173
+
174
+ # A pooled session that sat idle while dash was busy elsewhere (a server-lock
175
+ # wait, a loadbalancer reboot) gets dropped by NATs and cloud networks without
176
+ # either end noticing: net-ssh only sends keepalives from inside its event
177
+ # loop, and the pool's liveness probe is a non-blocking `process(0)`, so the
178
+ # session looks fine until the first command on it dies. Evict that session
179
+ # and rerun the block once on a fresh connection.
180
+ #
181
+ # The block is rerun, so a drop *mid-command* runs the command twice. That
182
+ # is accepted: the errors here are the idle-drop ones, where nothing reached
183
+ # the server, and the alternative is the deploy failing on the next host.
184
+ module ReconnectOnStaleConnection
185
+ STALE_CONNECTION_ERRORS = [ Errno::ECONNRESET, Errno::EPIPE, Net::SSH::Disconnect, Net::SSH::Timeout ].freeze
186
+
187
+ private
188
+ def with_ssh
189
+ reconnected = false
190
+
191
+ begin
192
+ super do |ssh|
193
+ yield ssh
194
+ rescue *STALE_CONNECTION_ERRORS
195
+ evict_stale_session(ssh)
196
+ raise
197
+ end
198
+ rescue *STALE_CONNECTION_ERRORS => e
199
+ raise if reconnected
200
+
201
+ reconnected = true
202
+ SSHKit.config.output.warn("Reconnecting to #{host}: #{e.message}")
203
+ retry
204
+ end
205
+ end
206
+
207
+ # `close` waits for channel-close acknowledgements, which never come over
208
+ # a dead socket. `shutdown!` just closes the socket, and a closed session
209
+ # is what makes the pool drop it instead of caching it again.
210
+ def evict_stale_session(ssh)
211
+ ssh.shutdown!
212
+ rescue StandardError
213
+ nil
214
+ end
215
+ end
216
+ prepend ReconnectOnStaleConnection
173
217
  end
174
218
 
175
219
  class SSHKit::Runner::Parallel
data/lib/dash/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Dash
2
- VERSION = "4.0.2"
2
+ VERSION = "4.0.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: 4.0.2
4
+ version: 4.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson