indocker 0.5.1 → 0.5.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: 3a337d7800f2098f536fa3c6c070440e3a4e7ace58945d9a5e70127bb4350de3
4
- data.tar.gz: '097fc4489b74f7ce5c49415e6ba2a662ecf15af95ec5b3d8a7128ec09b0b2f62'
3
+ metadata.gz: 70bf1241f0783dede869906f9f359c2a2137778a3059acaea9ae2fc7db30bb38
4
+ data.tar.gz: f4d906fc4ae36b9bf4482c7e5802c03b4a8633c7f839a0ffb71bbfc1571a5e30
5
5
  SHA512:
6
- metadata.gz: fa93cd90ee34cf29b145dbc17493c18e419d1808b6cd3748d71ca712037f3b0bcf4ef8ceb88fdd00ef755dae44a1712d8547d6cfc2c46deba602ececdac26b4f
7
- data.tar.gz: 2306ab2913402242c95a989c29b18cf918310889e4e1524764cdbe1e2e0d01d959d0cba2b912b4834d168e89c422bab0a4a7800f173faac9253135a1536d5a3b
6
+ metadata.gz: d45a4d6ab954e10b76fbaf2b4ca098c1368fd6b6365756f24bb210757d5f221d0fcfcf5b11b378cc5e31de38d72967158c90d91aaa3c9df53334f8a2596f12f0
7
+ data.tar.gz: a0ae86f49931e812d34761ff20f73f704a010b742edb1968dae8e70fc7798edbd75cab519f34632d8f6840ab0b87f5454c70773c50627ac10fc5693e263819cb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- indocker (0.5.1)
4
+ indocker (0.5.4)
5
5
  net-ssh
6
6
 
7
7
  GEM
@@ -20,29 +20,29 @@ class Indocker::ContainerDeployer
20
20
  def deploy(container, force_restart, skip_force_restart, progress)
21
21
  return if @deployed_containers[container]
22
22
 
23
- # Deploy the container to all of its servers in parallel, each server in its
24
- # own thread with its own ssh connection.
25
- #
26
- # Per-server exclusion: only one container may deploy on a given server at a
27
- # time, so several container types don't spike CPU/memory by starting at once
28
- # on the same host. We grab the lock of every target server before deploying.
29
- # Locks are always taken in a consistent (sorted) order, so concurrent
30
- # deployments of containers with overlapping server sets can't deadlock.
31
- ordered_servers = container.servers.sort_by { |server| server_lock_key(server) }
32
- locks = ordered_servers.map { |server| server_lock(server) }.uniq
33
-
34
- with_locks(locks) do
35
- threads = container.servers.map do |server|
36
- Thread.new do
23
+ exit_codes =
24
+ if deploy_servers_in_parallel?(container)
25
+ # Stateless containers: deploy to all servers at once.
26
+ threads = container.servers.map do |server|
27
+ Thread.new do
28
+ deploy_to_server(container, server, force_restart, skip_force_restart, progress)
29
+ end
30
+ end
31
+
32
+ threads.map(&:value)
33
+ else
34
+ # Rolling deploy: one server at a time. Containers with rolling-restart
35
+ # hooks (e.g. draining/enabling an nginx upstream) sit behind a load
36
+ # balancer, so all of their servers must not restart simultaneously —
37
+ # otherwise there is a moment with no live backend (502s). Deploying
38
+ # server by server keeps the rest of the fleet serving.
39
+ container.servers.map do |server|
37
40
  deploy_to_server(container, server, force_restart, skip_force_restart, progress)
38
41
  end
39
42
  end
40
43
 
41
- results = threads.map(&:value)
42
-
43
- if results.any? { |exit_code| exit_code != 0 }
44
- exit 1
45
- end
44
+ if exit_codes.any? { |exit_code| exit_code != 0 }
45
+ exit 1
46
46
  end
47
47
 
48
48
  @deployed_containers[container] = true
@@ -56,52 +56,57 @@ class Indocker::ContainerDeployer
56
56
 
57
57
  private
58
58
 
59
+ # Whether the container may be deployed to all of its servers concurrently.
60
+ # An explicit :parallel_deploy start option wins; otherwise a container is
61
+ # deployed in parallel only when it has no rolling-restart hooks (containers
62
+ # with before/after start hooks are drained at a load balancer and must roll
63
+ # across servers one at a time).
64
+ def deploy_servers_in_parallel?(container)
65
+ option = container.get_start_option(:parallel_deploy)
66
+ return option unless option.nil?
67
+
68
+ container.before_start_proc.nil? && container.after_start_proc.nil?
69
+ end
70
+
59
71
  # Runs a single container deployment on a single server. Returns the remote
60
- # exit code (0 on success). Runs inside its own thread with a dedicated ssh
61
- # connection, so it must not touch shared mutable state except the
62
- # thread-safe `progress`.
72
+ # exit code (0 on success).
73
+ #
74
+ # Per-server exclusion: only one container deploys on a given server at a time,
75
+ # so several container types don't spike CPU/memory by starting at once on the
76
+ # same host. Each invocation holds only its own server's lock, so concurrent
77
+ # deployments can't deadlock.
63
78
  def deploy_to_server(container, server, force_restart, skip_force_restart, progress)
64
- progress.start_deploying_container(container, server)
65
-
66
- deploy_server = @server_pool.create_connection!(server)
67
- @logger.info("Deploying container: #{container.name.to_s.green} to #{server.user}@#{server.host}")
79
+ server_lock(server).synchronize do
80
+ progress.start_deploying_container(container, server)
81
+
82
+ deploy_server = @server_pool.create_connection!(server)
83
+ @logger.info("Deploying container: #{container.name.to_s.green} to #{server.user}@#{server.host}")
84
+
85
+ result = deploy_server
86
+ .run_container_remotely(
87
+ configuration_name: Indocker.configuration_name,
88
+ container_name: container.name,
89
+ force_restart: force_restart && !skip_force_restart.include?(container.name)
90
+ )
91
+
92
+ if result.exit_code == 0
93
+ @logger.info("Container deployment to #{server.user}@#{server.host} finished: #{container.name.to_s.green}")
94
+ progress.finish_deploying_container(container, server)
95
+ end
68
96
 
69
- result = deploy_server
70
- .run_container_remotely(
71
- configuration_name: Indocker.configuration_name,
72
- container_name: container.name,
73
- force_restart: force_restart && !skip_force_restart.include?(container.name)
74
- )
97
+ deploy_server.close_session
75
98
 
76
- if result.exit_code == 0
77
- @logger.info("Container deployment to #{server.user}@#{server.host} finished: #{container.name.to_s.green}")
78
- progress.finish_deploying_container(container, server)
99
+ result.exit_code
79
100
  end
80
-
81
- deploy_server.close_session
82
-
83
- result.exit_code
84
- end
85
-
86
- def server_lock_key(server)
87
- [server.host, server.port, server.user]
88
101
  end
89
102
 
90
103
  # Returns a process-wide singleton Mutex for the given server, so the same lock
91
104
  # is shared across every container targeting that server.
92
105
  def server_lock(server)
93
- key = server_lock_key(server)
106
+ key = [server.host, server.port, server.user]
94
107
 
95
108
  @server_locks_mutex.synchronize do
96
109
  @server_locks[key] ||= Mutex.new
97
110
  end
98
111
  end
99
-
100
- def with_locks(locks, &block)
101
- if locks.empty?
102
- block.call
103
- else
104
- locks.first.synchronize { with_locks(locks.drop(1), &block) }
105
- end
106
- end
107
112
  end
@@ -1,9 +1,7 @@
1
- require 'timeout'
2
1
  require 'benchmark'
3
2
  require 'tempfile'
4
3
 
5
4
  class Indocker::Launchers::ConfigurationDeployer
6
- REMOTE_OPERATION_TIMEOUT = 60
7
5
 
8
6
  def initialize(logger:, global_logger:)
9
7
  Thread.abort_on_exception = true # abort all threads if exception occurs
@@ -107,12 +105,8 @@ class Indocker::Launchers::ConfigurationDeployer
107
105
 
108
106
  def wait_remote_operations(remote_operations)
109
107
  remote_operations.each do |remote_operation|
110
- begin
111
- Timeout::timeout(REMOTE_OPERATION_TIMEOUT) do
112
- remote_operation.thread.join
113
- end
114
- rescue Timeout::Error
115
- @global_logger.error("Deployment aborted. Remote operation :#{remote_operation.operation} on server #{remote_operation.server.user}@#{remote_operation.server.host} did not finish during #{REMOTE_OPERATION_TIMEOUT} seconds.")
108
+ if remote_operation.thread.join(Indocker.remote_operation_timeout).nil?
109
+ @global_logger.error("Deployment aborted. Remote operation :#{remote_operation.operation} on server #{remote_operation.server.user}@#{remote_operation.server.host} did not finish during #{Indocker.remote_operation_timeout} seconds.")
116
110
  exit 1
117
111
  end
118
112
  end
@@ -302,34 +296,28 @@ class Indocker::Launchers::ConfigurationDeployer
302
296
  # container to the deploy stage — so the next image is already building while
303
297
  # the previous container deploys.
304
298
  #
305
- # The consumer starts a deploy per built container. Each deploy first waits for
306
- # all of the container's dependencies to finish deploying (depends_on ordering),
307
- # then deploys. Independent containers deploy concurrently; per-server exclusion
308
- # (one container at a time per host) is enforced inside ContainerDeployer.
299
+ # The consumer deploys containers strictly one at a time, in the order they
300
+ # were built (which is dependency order, so depends_on is respected). Only a
301
+ # single container is ever restarting across the whole fleet at any moment —
302
+ # the invariant the original sequential deploy guaranteed. This matters for
303
+ # zero-downtime web apps: deploying several containers concurrently can flap an
304
+ # app's health (its health check depends on backends that would be restarting
305
+ # in parallel), leaving nginx with a live-but-unhealthy upstream and serving
306
+ # 502s during the deploy.
307
+ #
308
+ # Only image builds run ahead of deploys (below) — that is where the pipeline
309
+ # speedup comes from. A single container still fans out to its own servers in
310
+ # parallel (see ContainerDeployer), so multi-server containers stay fast.
309
311
  def pipeline_build_and_deploy(configuration, deployer, build_server_pool, containers,
310
312
  skip_build, skip_deploy, force_restart, skip_force_restart)
311
313
 
312
314
  ordered_containers = build_deploy_order(containers)
313
315
 
314
- deploy_queue = Queue.new
315
- deploy_threads = {}
316
+ deploy_queue = Queue.new
316
317
 
317
318
  consumer = Thread.new do
318
319
  while (container = deploy_queue.pop) != :done
319
- # Dependencies are always built (and therefore queued) before their
320
- # dependents, so their deploy threads already exist here.
321
- dependency_threads = container
322
- .dependent_containers
323
- .map { |dependency| deploy_threads[dependency] }
324
- .compact
325
-
326
- # Pass container/dependency_threads as thread arguments so each thread
327
- # captures its own value — the shared loop variables get reassigned on
328
- # the next iteration (eventually to :done) before these threads run.
329
- deploy_threads[container] = Thread.new(container, dependency_threads) do |container, dependency_threads|
330
- dependency_threads.each(&:join)
331
- deploy_container(deployer, container, force_restart, skip_force_restart)
332
- end
320
+ deploy_container(deployer, container, force_restart, skip_force_restart)
333
321
  end
334
322
  end
335
323
 
@@ -351,7 +339,6 @@ class Indocker::Launchers::ConfigurationDeployer
351
339
 
352
340
  deploy_queue.push(:done)
353
341
  consumer.join
354
- deploy_threads.values.each(&:join)
355
342
  end
356
343
 
357
344
  # Returns the containers to deploy in dependency order (each container's
@@ -467,11 +454,13 @@ class Indocker::Launchers::ConfigurationDeployer
467
454
 
468
455
  @logger.info("Updating crontab file #{deploy_user}:#{crontab_filepath}")
469
456
 
470
- Indocker::Shell.command("scp #{tmp_crontab_file.path} #{deploy_user}:#{crontab_filepath}", @logger)
457
+ ssh_options = "-o BatchMode=yes -o ConnectTimeout=#{Indocker.ssh_connect_timeout}"
458
+
459
+ Indocker::Shell.command("scp #{ssh_options} #{tmp_crontab_file.path} #{deploy_user}:#{crontab_filepath}", @logger)
471
460
 
472
461
  tmp_crontab_file.unlink
473
462
 
474
- Indocker::Shell.command("ssh #{deploy_user} 'crontab #{crontab_filepath}'", @logger)
463
+ Indocker::Shell.command("ssh #{ssh_options} #{deploy_user} 'crontab #{crontab_filepath}'", @logger)
475
464
  end
476
465
 
477
466
  def sync_indocker(servers)
@@ -29,7 +29,20 @@ class Indocker::Rsync
29
29
  sync_local_cp(from, to, raise_on_error: raise_on_error)
30
30
  end
31
31
  else
32
- command = "rsync --delete-after -a -e 'ssh -p #{session.port}' #{from} #{session.user}@#{session.host}:#{to}"
32
+ # BatchMode/ConnectTimeout: an unreachable server must fail in seconds with
33
+ # a clear rsync error instead of hanging silently (or prompting for a password)
34
+ # until the deployment-wide remote operation timeout kills the whole deploy.
35
+ # ServerAlive*: a path that accepts the connection and then stops delivering
36
+ # packets (seen when a cloud NAT address half-works) would otherwise hang
37
+ # just as long — keepalives tear such a session down in ~30s.
38
+ ssh_command = [
39
+ "ssh -p #{session.port}",
40
+ "-o BatchMode=yes",
41
+ "-o ConnectTimeout=#{Indocker.ssh_connect_timeout}",
42
+ "-o ServerAliveInterval=10",
43
+ "-o ServerAliveCountMax=3",
44
+ ].join(' ')
45
+ command = "rsync --delete-after -a -e '#{ssh_command}' #{from} #{session.user}@#{session.host}:#{to}"
33
46
  Indocker.logger.debug("sync #{from} #{session.user}@#{session.host}:#{to}")
34
47
  Indocker::Shell.command(command, Indocker.logger, raise_on_error: raise_on_error)
35
48
  end
@@ -29,7 +29,14 @@ class Indocker::SshSession
29
29
 
30
30
  if host != LOCALHOST
31
31
  require 'net/ssh'
32
- @ssh = Net::SSH.start(@host, @user, {port: @port})
32
+ @ssh = Net::SSH.start(@host, @user, {
33
+ port: @port,
34
+ non_interactive: true,
35
+ timeout: Indocker.ssh_connect_timeout,
36
+ keepalive: true,
37
+ keepalive_interval: 10,
38
+ keepalive_maxcount: 3,
39
+ })
33
40
  end
34
41
  end
35
42
 
@@ -1,3 +1,3 @@
1
1
  module Indocker
2
- VERSION = "0.5.1"
2
+ VERSION = "0.5.4"
3
3
  end
data/lib/indocker.rb CHANGED
@@ -135,6 +135,22 @@ module Indocker
135
135
  @redeploy_crontab_path = val
136
136
  end
137
137
 
138
+ def set_remote_operation_timeout(seconds)
139
+ @remote_operation_timeout = seconds
140
+ end
141
+
142
+ def remote_operation_timeout
143
+ @remote_operation_timeout || 300
144
+ end
145
+
146
+ def set_ssh_connect_timeout(seconds)
147
+ @ssh_connect_timeout = seconds
148
+ end
149
+
150
+ def ssh_connect_timeout
151
+ @ssh_connect_timeout || 15
152
+ end
153
+
138
154
  def redeploy_crontab_path
139
155
  @redeploy_crontab_path
140
156
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: indocker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruslan Gatiyatov
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2026-07-06 00:00:00.000000000 Z
12
+ date: 2026-08-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: net-ssh