dash 4.0.4 → 4.0.7

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: 762f586250ded9a4e4f60850a91b1358bd993bcb1d4c6e291dfec93dc6d516e2
4
- data.tar.gz: d87d3161396bfdb38f9feb3d3946d0830bdd2b1dc2921e569a275e65f4285cb1
3
+ metadata.gz: d13e0fdb207715506d85fa85be2a719ca70640130f22ca948c075fd787b3dd88
4
+ data.tar.gz: cc42a4346178f4ffbcf7b7b2f03820e49f99e9d54c78ab864493a0c0b8b214e4
5
5
  SHA512:
6
- metadata.gz: 86a1e05606121be7d85b83c2da117d8bcc2a2badbf080a5e5d0237b3175dd98ac9abba3d1afa154495d6be85923d8ef7d915e3bb877241b69a637ab47de00658
7
- data.tar.gz: 9bca1f4085e3ad18ca778eba0dc3344b6592404839fdb4628a2a1bc05d81d869c4b4a837a18f66659f2c0b9fba307da4f14d877f0c088f709d72806a68bf2927
6
+ metadata.gz: cdf2135081cc1acc7de1d73d7c7156f93262ad82a5848a6db6aaf484bf4859ed3e529fede60ac0c67df99ec021acdd2cbf6a9175982af8f31873afa03cc2113b
7
+ data.tar.gz: 30eae7e9f41f2b216be4dd8d16ce583f5a8e2392d3b05b3ad15fcd4ba40645441c05c39a643cca2bf01876ee28aec2fa668a7ffe2a474e27c69b46edc341dbc6
data/bin/dash CHANGED
@@ -3,6 +3,11 @@
3
3
  # Prevent failures from being reported twice.
4
4
  Thread.report_on_exception = false
5
5
 
6
+ # Ruby block-buffers stdout when it is not a TTY, which is every CI runner. Line-by-line
7
+ # writes keep the runner's timestamps attributable to the command that printed them.
8
+ $stdout.sync = true
9
+ $stderr.sync = true
10
+
6
11
  require "dash"
7
12
 
8
13
  begin
@@ -14,22 +14,26 @@ class Dash::Cli::App::Boot
14
14
  end
15
15
 
16
16
  def run
17
- old_version = old_version_renamed_if_clashing
17
+ DASH.timings.phase("#{role} #{host}", depth: 1) do |timing|
18
+ @timing = timing
18
19
 
19
- wait_at_barrier if queuer?
20
+ old_version = old_version_renamed_if_clashing
20
21
 
21
- begin
22
- start_new_version
23
- rescue => e
24
- close_barrier if gatekeeper?
25
- stop_new_version
26
- raise
27
- end
22
+ wait_at_barrier if queuer?
23
+
24
+ begin
25
+ start_new_version
26
+ rescue => e
27
+ close_barrier if gatekeeper?
28
+ stop_new_version
29
+ raise
30
+ end
28
31
 
29
- release_barrier if gatekeeper?
32
+ release_barrier if gatekeeper?
30
33
 
31
- if old_version
32
- stop_old_version(old_version)
34
+ if old_version
35
+ stop_old_version(old_version)
36
+ end
33
37
  end
34
38
  end
35
39
 
@@ -59,10 +63,10 @@ class Dash::Cli::App::Boot
59
63
 
60
64
  run_hook "pre-proxy-deploy", hosts: host.to_s, role: role.name
61
65
  info "Deploying #{role} on #{host} via dash-proxy (waiting up to #{DASH.config.deploy_timeout}s for it to become healthy)..."
62
- execute *app.deploy(target: endpoint)
66
+ timing_healthy { execute *app.deploy(target: endpoint) }
63
67
  run_hook "post-proxy-deploy", hosts: host.to_s, role: role.name
64
68
  else
65
- Dash::Cli::Healthcheck::Poller.wait_for_healthy(role: role) { health_status }
69
+ timing_healthy { Dash::Cli::Healthcheck::Poller.wait_for_healthy(role: role) { health_status } }
66
70
  end
67
71
  rescue => e
68
72
  error "Failed to boot #{role} on #{host}"
@@ -136,6 +140,15 @@ class Dash::Cli::App::Boot
136
140
  end
137
141
  end
138
142
 
143
+ # The readiness wait is the part of a host's boot an operator can actually tune
144
+ # (health check interval, app boot time), so it gets called out on the host's entry.
145
+ def timing_healthy
146
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
147
+ yield
148
+ ensure
149
+ @timing.detail = format("healthy after %.1fs", Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
150
+ end
151
+
139
152
  def barrier_role?
140
153
  role == DASH.primary_role
141
154
  end
data/lib/dash/cli/app.rb CHANGED
@@ -426,10 +426,24 @@ class Dash::Cli::App < Dash::Cli::Base
426
426
  end
427
427
  end
428
428
 
429
+ # Canary hosts each form a group of one and go first; whatever is left is sliced by
430
+ # `limit` (a percentage counts those remaining hosts, the set it actually slices) or
431
+ # boots as a single group. No canary and no limit is the one-group default.
429
432
  def host_boot_groups
430
433
  hosts = DASH.app_hosts
431
- limit = DASH.config.boot.limit_for(hosts)
434
+ canary_hosts = canary_boot_hosts(hosts)
435
+ rest = hosts - canary_hosts
436
+ limit = DASH.config.boot.limit_for(rest)
432
437
 
433
- limit ? hosts.each_slice(limit).to_a : [ hosts ]
438
+ rest_groups = limit ? rest.each_slice(limit).to_a : [ rest ]
439
+
440
+ canary_hosts.map { |host| [ host ] } + rest_groups.reject(&:empty?)
441
+ end
442
+
443
+ # The first N primary-role hosts of this run. `--roles`/`--hosts` may leave none, in
444
+ # which case there is nothing to prove ahead of the others and no canary group.
445
+ def canary_boot_hosts(hosts)
446
+ count = DASH.config.boot.canary
447
+ count ? (DASH.primary_role.hosts & hosts).first(count) : []
434
448
  end
435
449
  end
data/lib/dash/cli/base.rb CHANGED
@@ -140,13 +140,22 @@ module Dash::Cli
140
140
  end
141
141
  end
142
142
 
143
+ # `setup` wraps `deploy`, so this nests. Each level reports its own total; the phase
144
+ # table belongs to the outermost, which is the one that saw every phase.
143
145
  def print_runtime
144
146
  started_at = Time.now
147
+ @print_runtime_depth = @print_runtime_depth.to_i + 1
145
148
  yield
146
149
  Time.now - started_at
147
150
  ensure
151
+ @print_runtime_depth -= 1
148
152
  runtime = Time.now - started_at
149
153
  puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
154
+ puts DASH.timings.lines if @print_runtime_depth.zero? && DASH.timings.any?
155
+ end
156
+
157
+ def timed(name, depth: 0, &block)
158
+ DASH.timings.phase(name, depth: depth, &block)
150
159
  end
151
160
 
152
161
  # Deploy lock outside, server lock inside. Every caller acquires in that
@@ -113,7 +113,12 @@ class Dash::Cli::Doctor::HostChecks
113
113
  elsif version.nil?
114
114
  result :proxy_version, :ok, "not running (will be started on deploy)"
115
115
  elsif Dash::Utils.older_version?(version, minimum)
116
- result :proxy_version, :fail, "#{version} is older than the minimum #{minimum}, run `dash proxy reboot` to update"
116
+ if deploy_reboots_proxy?
117
+ result :proxy_version, :ok,
118
+ "#{version} is older than the minimum #{minimum}; the next deploy reboots the proxy to update it"
119
+ else
120
+ result :proxy_version, :fail, "#{version} is older than the minimum #{minimum}, run `dash proxy reboot` to update"
121
+ end
117
122
  else
118
123
  result :proxy_version, :ok, "#{version} (minimum #{minimum})"
119
124
  end
@@ -121,6 +126,18 @@ class Dash::Cli::Doctor::HostChecks
121
126
  result :proxy_version, :warn, "running image tag #{version} is not a version number"
122
127
  end
123
128
 
129
+ # Mirrors `proxy boot`: with reboot_on_deploy enabled it reboots a drifted
130
+ # proxy on its own, and a MINIMUM_VERSION bump moves the expected config
131
+ # digest, so a merely-stale proxy is converged by the very deploy this
132
+ # doctor gates. Only a version the deploy would not touch — pinned in the
133
+ # config, or with automatic reboots disabled — is a real failure. Any error
134
+ # probing drift falls back to the conservative answer.
135
+ def deploy_reboots_proxy?
136
+ DASH.config.proxy.reboot_on_deploy? && Dash::Cli::Proxy::Drift.new(host, sshkit).drifted?
137
+ rescue StandardError
138
+ false
139
+ end
140
+
124
141
  # What a container path has to look like to count as a container runtime
125
142
  # socket when the config no longer names one.
126
143
  DOCKER_SOCKET_PATTERN = %r{docker\.sock\z}
data/lib/dash/cli/main.rb CHANGED
@@ -8,7 +8,7 @@ class Dash::Cli::Main < Dash::Cli::Base
8
8
  invoke_options = deploy_options
9
9
 
10
10
  say "Ensure Docker is installed...", :magenta
11
- invoke "dash:cli:server:bootstrap", [], invoke_options
11
+ timed("Ensure Docker is installed") { invoke "dash:cli:server:bootstrap", [], invoke_options }
12
12
 
13
13
  deploy(boot_accessories: true)
14
14
  end
@@ -30,32 +30,32 @@ class Dash::Cli::Main < Dash::Cli::Base
30
30
 
31
31
  if options[:skip_push]
32
32
  say "Pull app image...", :magenta
33
- invoke "dash:cli:build:pull", [], invoke_options
33
+ timed("Pull app image") { invoke "dash:cli:build:pull", [], invoke_options }
34
34
  else
35
35
  say "Build and push app image...", :magenta
36
- invoke "dash:cli:build:deliver", [], invoke_options
36
+ timed("Build and push app image") { invoke "dash:cli:build:deliver", [], invoke_options }
37
37
  end
38
38
 
39
39
  modify(lock: true) do
40
40
  run_hook "pre-deploy", secrets: true
41
41
 
42
42
  say "Ensure dash-proxy is running...", :magenta
43
- invoke "dash:cli:proxy:boot", [], invoke_options
43
+ timed("Ensure dash-proxy") { invoke "dash:cli:proxy:boot", [], invoke_options }
44
44
 
45
- invoke "dash:cli:accessory:boot", [ "all" ], invoke_options if boot_accessories
45
+ timed("Boot accessories") { invoke "dash:cli:accessory:boot", [ "all" ], invoke_options } if boot_accessories
46
46
 
47
47
  say "Detect stale containers...", :magenta
48
- invoke "dash:cli:app:stale_containers", [], invoke_options.merge(stop: true)
48
+ timed("Detect stale containers") { invoke "dash:cli:app:stale_containers", [], invoke_options.merge(stop: true) }
49
49
 
50
- invoke "dash:cli:app:boot", [], invoke_options
50
+ timed("Boot") { invoke "dash:cli:app:boot", [], invoke_options }
51
51
 
52
52
  if DASH.config.proxy.load_balancing?
53
53
  say "Updating loadbalancer configuration...", :magenta
54
- invoke "dash:cli:proxy:loadbalancer", [ "deploy" ], invoke_options
54
+ timed("Loadbalancer") { invoke "dash:cli:proxy:loadbalancer", [ "deploy" ], invoke_options }
55
55
  end
56
56
 
57
57
  say "Prune old containers and images...", :magenta
58
- invoke "dash:cli:prune:all", [], invoke_options
58
+ timed("Prune") { invoke "dash:cli:prune:all", [], invoke_options }
59
59
  end
60
60
  end
61
61
 
@@ -78,23 +78,23 @@ class Dash::Cli::Main < Dash::Cli::Base
78
78
 
79
79
  if options[:skip_push]
80
80
  say "Pull app image...", :magenta
81
- invoke "dash:cli:build:pull", [], invoke_options
81
+ timed("Pull app image") { invoke "dash:cli:build:pull", [], invoke_options }
82
82
  else
83
83
  say "Build and push app image...", :magenta
84
- invoke "dash:cli:build:deliver", [], invoke_options
84
+ timed("Build and push app image") { invoke "dash:cli:build:deliver", [], invoke_options }
85
85
  end
86
86
 
87
87
  modify(lock: true) do
88
88
  run_hook "pre-deploy", secrets: true
89
89
 
90
90
  say "Detect stale containers...", :magenta
91
- invoke "dash:cli:app:stale_containers", [], invoke_options.merge(stop: true)
91
+ timed("Detect stale containers") { invoke "dash:cli:app:stale_containers", [], invoke_options.merge(stop: true) }
92
92
 
93
- invoke "dash:cli:app:boot", [], invoke_options
93
+ timed("Boot") { invoke "dash:cli:app:boot", [], invoke_options }
94
94
 
95
95
  if DASH.config.proxy.load_balancing?
96
96
  say "Updating loadbalancer configuration...", :magenta
97
- invoke "dash:cli:proxy:loadbalancer", [ "deploy" ], invoke_options
97
+ timed("Loadbalancer") { invoke "dash:cli:proxy:loadbalancer", [ "deploy" ], invoke_options }
98
98
  end
99
99
  end
100
100
  end
@@ -117,7 +117,7 @@ class Dash::Cli::Main < Dash::Cli::Base
117
117
  if container_available?(version)
118
118
  run_hook "pre-deploy", secrets: true
119
119
 
120
- invoke "dash:cli:app:boot", [], invoke_options.merge(version: version)
120
+ timed("Boot") { invoke "dash:cli:app:boot", [], invoke_options.merge(version: version) }
121
121
  rolled_back = true
122
122
  else
123
123
  say "The app version '#{version}' is not available as a container (use 'dash app containers' for available versions)", :red
@@ -6,7 +6,7 @@ require "active_support/notifications"
6
6
 
7
7
  class Dash::Commander
8
8
  attr_accessor :verbosity, :holding_lock, :holding_server_lock, :connected, :logging, :lock_wait, :lock_wait_timeout, :lock_wait_interval
9
- attr_reader :specific_roles, :specific_hosts
9
+ attr_reader :specific_roles, :specific_hosts, :timings
10
10
  delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :app_hosts, :proxy_hosts, :accessory_hosts, to: :specifics
11
11
 
12
12
  def initialize
@@ -23,6 +23,7 @@ class Dash::Commander
23
23
  self.lock_wait_timeout = 900
24
24
  self.lock_wait_interval = 15
25
25
  @modify_depth = 0
26
+ @timings = Dash::Timings.new
26
27
  @specifics = @specific_roles = @specific_hosts = nil
27
28
  @config = @config_kwargs = nil
28
29
  @output_logger = nil
@@ -8,6 +8,7 @@ class Dash::Configuration::Boot
8
8
  def initialize(config:, boot_config: nil, context: nil)
9
9
  @boot_config = boot_config || config.raw_config.boot || {}
10
10
  validate! @boot_config, context: context
11
+ validate_canary!(context || self.class.validation_config_key)
11
12
  end
12
13
 
13
14
  # The group size to slice `hosts` by. A percentage only means something against the set
@@ -35,6 +36,22 @@ class Dash::Configuration::Boot
35
36
  boot_config["wait"]
36
37
  end
37
38
 
39
+ # How many primary-role hosts boot alone, one at a time, before the rest boot together.
40
+ # Whole-deploy only — Cli::App#host_boot_groups is the sole consumer.
41
+ def canary
42
+ boot_config["canary"]
43
+ end
44
+
45
+ def canary?
46
+ canary.present?
47
+ end
48
+
49
+ # Whether this config splits hosts into more than one group at all — what `wait` needs
50
+ # in order to mean anything.
51
+ def groups?
52
+ limit? || canary?
53
+ end
54
+
38
55
  def parallel_roles
39
56
  boot_config["parallel_roles"]
40
57
  end
@@ -57,4 +74,20 @@ class Dash::Configuration::Boot
57
74
  { in: :groups, limit: limit, wait: wait.to_i }
58
75
  end
59
76
  end
77
+
78
+ private
79
+ # The example-driven validator has already checked the type; the range and the
80
+ # top-level-only rule are ours. A role-scoped Boot always carries a context.
81
+ def validate_canary!(context)
82
+ return unless canary?
83
+
84
+ if context != self.class.validation_config_key
85
+ raise Dash::ConfigurationError, "#{context}/canary is only supported at the top-level boot: " \
86
+ "it slices the primary role's hosts ahead of every other role"
87
+ end
88
+
89
+ if canary < 1
90
+ raise Dash::ConfigurationError, "#{context}/canary must be at least 1, got #{canary}"
91
+ end
92
+ end
60
93
  end
@@ -26,6 +26,20 @@ boot:
26
26
  # moves on as soon as the last host is up.
27
27
  wait: 10
28
28
 
29
+ # The number of primary-role hosts to boot one at a time before the rest of the hosts boot.
30
+ #
31
+ # A canary is the gate people reach for `limit: 1` to get — prove the new version on one host
32
+ # before touching the fleet — without serialising every host after it. The first N hosts of the
33
+ # primary role each boot alone; a boot failure there stops the deploy before any other host is
34
+ # touched. The remaining hosts of every role then boot together, or in groups when `limit` is
35
+ # also set (a percentage limit counts those remaining hosts). `wait` paces canary and remaining
36
+ # groups alike. Behind dash-proxy this never reduces capacity: each host keeps serving the old
37
+ # container until the new one passes its health check.
38
+ #
39
+ # Top-level only. A role-scoped `boot` refuses it: the canary slices the primary role's hosts
40
+ # ahead of every other role, which a single role cannot express.
41
+ canary: 1
42
+
29
43
  # Whether to boot roles in parallel on a host.
30
44
  #
31
45
  # If a host has multiple roles, control whether they are booted in parallel or sequentially on that host.
@@ -82,7 +82,7 @@ asset_path: /path/to/assets
82
82
  # Hooks path
83
83
  #
84
84
  # Path to hooks, defaults to `.dash/hooks` (`.kamal/hooks` is still read when only that exists).
85
- # See https://kamal-deploy.org/docs/hooks for more information:
85
+ # See https://dash.zoolutions.llc/docs/hooks for every hook, when it fires, and its environment:
86
86
  hooks_path: /user_home/kamal/hooks
87
87
 
88
88
  # Hook output
@@ -31,7 +31,7 @@ env:
31
31
  # RAILS_MASTER_KEY=$(cat config/master.key)
32
32
  # ```
33
33
  #
34
- # You can also use [secret helpers](../../commands/secrets) for some common password managers.
34
+ # You can also use [secret helpers](https://dash.zoolutions.llc/docs/secrets) for common password managers and secret stores.
35
35
  #
36
36
  # ```shell
37
37
  # SECRETS=$(dash secrets fetch ...)
@@ -834,7 +834,7 @@ proxy:
834
834
  # below already embeds its ghcr.io host
835
835
  repository: ghcr.io/zoolutions/dash-proxy # Container repository for the
836
836
  # dash-proxy image (this is the default)
837
- version: v1.1.0.0 # Version tag of the dash-proxy image to use.
837
+ version: v1.1.0.1 # Version tag of the dash-proxy image to use.
838
838
  # Defaults to the minimum version this gem
839
839
  # requires - only pin it to roll forward early,
840
840
  # never below the default
@@ -1,5 +1,5 @@
1
1
  class Dash::Configuration::Proxy::Run
2
- MINIMUM_VERSION = "v1.1.0.0"
2
+ MINIMUM_VERSION = "v1.1.0.1"
3
3
  DEFAULT_HTTP_PORT = 80
4
4
  DEFAULT_HTTPS_PORT = 443
5
5
  DEFAULT_LOG_MAX_SIZE = "10m"
@@ -4,6 +4,7 @@ class Dash::Configuration::Registry
4
4
  def initialize(config:, secrets:, context: "registry")
5
5
  @registry_config = config["registry"] || {}
6
6
  @secrets = secrets
7
+ @context = context
7
8
  validate! registry_config, context: context, with: Dash::Configuration::Validator::Registry
8
9
  end
9
10
 
@@ -28,13 +29,33 @@ class Dash::Configuration::Registry
28
29
  end
29
30
 
30
31
  private
31
- attr_reader :registry_config, :secrets
32
+ attr_reader :registry_config, :secrets, :context
32
33
 
34
+ # A blank credential would reach the remote host as an empty `docker login -p`,
35
+ # where docker falls back to prompting and dies with the opaque "Cannot perform
36
+ # an interactive login from a non TTY device" — fail locally instead.
33
37
  def lookup(key)
38
+ value =
39
+ if registry_config[key].is_a?(Array)
40
+ secrets[registry_config[key].first]
41
+ else
42
+ registry_config[key]
43
+ end
44
+
45
+ validate_present! key, value unless local?
46
+ value
47
+ end
48
+
49
+ def validate_present!(key, value)
50
+ return if value.present?
51
+
34
52
  if registry_config[key].is_a?(Array)
35
- secrets[registry_config[key].first]
53
+ secret = registry_config[key].first
54
+ raise Dash::ConfigurationError,
55
+ "#{context}/#{key}: secret '#{secret}' resolved to an empty value — " \
56
+ "if your secrets file forwards it from the environment (#{secret}=$#{secret}), export the variable before running dash"
36
57
  else
37
- registry_config[key]
58
+ raise Dash::ConfigurationError, "#{context}/#{key}: is blank"
38
59
  end
39
60
  end
40
61
  end
@@ -592,17 +592,17 @@ class Dash::Configuration
592
592
  end
593
593
 
594
594
  # `wait` only ever fills the gap between one group of hosts and the next, and without a
595
- # `limit` there are no gaps — every host boots in one group. Before #47 that silently
595
+ # `limit` or a `canary` there are no gaps — every host boots in one group. Before #47 that silently
596
596
  # cost a deploy one `wait` interval; now it silently does nothing at all. Either way the
597
597
  # operator asked for staggering and is not getting it, so say so.
598
598
  def ensure_boot_wait_paces_something
599
599
  offenders = [ [ "boot", boot ], *roles.filter_map { |role| [ "servers/#{role.name}/boot", role.boot ] if role.boot } ]
600
- .select { |_context, boot_config| boot_config.wait.present? && !boot_config.limit? }
600
+ .select { |_context, boot_config| boot_config.wait.present? && !boot_config.groups? }
601
601
 
602
602
  offenders.each do |context, boot_config|
603
603
  warn "#{context}/wait is set to #{boot_config.wait} but #{context}/limit is not, so it does nothing. " \
604
604
  "`wait` paces one group of hosts against the next, and without a limit every host boots in a single group. " \
605
- "Set `#{context}/limit` to stagger the boot, or remove `#{context}/wait`."
605
+ "Set `#{context}/limit` or `#{context}/canary` to stagger the boot, or remove `#{context}/wait`."
606
606
  end
607
607
 
608
608
  true
@@ -0,0 +1,45 @@
1
+ # Wall-clock accounting for a deploy, printed under "Finished all in". The total on its
2
+ # own says nothing about where the time went — a serialised boot, a slow pull, or a proxy
3
+ # waiting on a health check all look the same from the outside.
4
+ #
5
+ # Entries land in start order and carry a depth, so a parent phase (Boot) prints above
6
+ # the host entries it wraps even though the hosts finish first. Boot runs one thread per
7
+ # host, so every mutation is behind the mutex.
8
+ class Dash::Timings
9
+ Entry = Struct.new(:name, :seconds, :detail, :depth)
10
+
11
+ def initialize
12
+ @entries = []
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ # Times the block. The entry is yielded so the block can annotate it — a boot can say
17
+ # how long of its total was spent waiting for the container to become healthy.
18
+ def phase(name, depth: 0)
19
+ entry = Entry.new(name, nil, nil, depth)
20
+ @mutex.synchronize { @entries << entry }
21
+ started = clock
22
+
23
+ yield entry
24
+ ensure
25
+ entry.seconds = clock - started
26
+ end
27
+
28
+ def any?
29
+ @mutex.synchronize { @entries.any? }
30
+ end
31
+
32
+ def lines
33
+ @mutex.synchronize do
34
+ @entries.map do |entry|
35
+ line = format(" %s%-36s %6.1fs", " " * entry.depth, entry.name, entry.seconds.to_f)
36
+ entry.detail ? "#{line} (#{entry.detail})" : line
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+ def clock
43
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
44
+ end
45
+ end
data/lib/dash/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Dash
2
- VERSION = "4.0.4"
2
+ VERSION = "4.0.7"
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.4
4
+ version: 4.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -329,6 +329,7 @@ files:
329
329
  - lib/dash/secrets/dotenv/inline_command_substitution.rb
330
330
  - lib/dash/sshkit_with_ext.rb
331
331
  - lib/dash/tags.rb
332
+ - lib/dash/timings.rb
332
333
  - lib/dash/utils.rb
333
334
  - lib/dash/utils/sensitive.rb
334
335
  - lib/dash/version.rb