kicks_liveness 0.1.1 → 0.1.3

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.
@@ -0,0 +1,110 @@
1
+ require 'fileutils'
2
+
3
+ module KicksLiveness
4
+ # Claims worker topology reports across fork respawns. Kept outside
5
+ # {Heartbeat} because the probe does not report these diagnostics.
6
+ #
7
+ # @see file:docs/LIMITATIONS.md#container-generations-need-linux-procfs-and-a-private-pid-namespace
8
+ # @api private
9
+ class TopologyReport
10
+ # @param dir [String] marks directory
11
+ # @param topology [ContainerGeneration::Result] what this worker proved
12
+ def initialize(dir, topology)
13
+ @dir = dir
14
+ @topology = topology
15
+ end
16
+
17
+ # Claims the right to report this state, once.
18
+ #
19
+ # Unverifiable results use a generation key; unavailable and unsafe results
20
+ # have no trustworthy generation and use only their state. Verification
21
+ # clears the flag so a later degradation can be reported again.
22
+ #
23
+ # @return [Array(Symbol, String), nil] the level and the line to log, or nil
24
+ # when there is nothing new to say
25
+ def claim!
26
+ key = key_for(@topology)
27
+
28
+ unless key
29
+ forget! if @topology.verified?
30
+ return nil
31
+ end
32
+
33
+ return nil unless claim(key)
34
+
35
+ report_for(@topology)
36
+ end
37
+
38
+ private
39
+
40
+ def path
41
+ File.join(@dir, 'topology-report')
42
+ end
43
+
44
+ def key_for(topology)
45
+ case topology.state
46
+ when :unsafe then 'unsafe'
47
+ when :unverifiable then "unverifiable #{topology.generation}"
48
+ when :unavailable then 'unavailable'
49
+ end
50
+ end
51
+
52
+ # Only unsafe changes the probe verdict and is reported at ERROR.
53
+ def report_for(topology)
54
+ case topology.state
55
+ when :unsafe
56
+ [:error, "#{Heartbeat::UNSUPPORTED_TOPOLOGY} (#{topology.detail}); " \
57
+ 'liveness probes fail until the pod is recreated without those settings']
58
+ when :unverifiable
59
+ [:warn, "PID topology unverified (#{topology.detail}): the container generation still scopes the heartbeat, " \
60
+ 'but shareProcessNamespace and hostPID cannot be ruled out']
61
+ else
62
+ [:warn, "no container generation (#{topology.detail}): falling back to freshness only, " \
63
+ 'which cannot reject a heartbeat inherited from a previous container']
64
+ end
65
+ end
66
+
67
+ # The runner starts all of its forks together, so the read and write have
68
+ # to be one inter-process operation: an atomic rename protects readers from
69
+ # a partial file, but does not stop every fork reading the old key and each
70
+ # claiming the same report. The state file is also the lock file, so no
71
+ # second path is added to the marks contract. Once it has existed it is
72
+ # cleared by truncation rather than unlink: every process must keep locking
73
+ # the same inode.
74
+ def claim(key)
75
+ synchronize(create: true) do |state|
76
+ state.rewind
77
+ next false if state.read.chomp == key
78
+
79
+ state.rewind
80
+ state.truncate(0)
81
+ state.write("#{key}\n")
82
+ state.flush
83
+ true
84
+ end
85
+ rescue StandardError
86
+ # A report that cannot be remembered must still be made. The cost is
87
+ # repetition on the next start, not silence about a lost guarantee.
88
+ true
89
+ end
90
+
91
+ def forget!
92
+ return unless File.exist?(path)
93
+
94
+ synchronize(create: false) { |state| state.truncate(0) }
95
+ rescue StandardError
96
+ nil
97
+ end
98
+
99
+ def synchronize(create:)
100
+ FileUtils.mkdir_p(@dir)
101
+ flags = File::RDWR
102
+ flags |= File::CREAT if create
103
+ File.open(path, flags, 0o644) do |state|
104
+ raise IOError, "cannot lock #{path}" unless state.flock(File::LOCK_EX)
105
+
106
+ yield state
107
+ end
108
+ end
109
+ end
110
+ end
@@ -1,4 +1,4 @@
1
1
  module KicksLiveness
2
2
  # @return [String] gem version
3
- VERSION = '0.1.1'.freeze
3
+ VERSION = '0.1.3'.freeze
4
4
  end
@@ -1,6 +1,7 @@
1
1
  require_relative 'kicks_liveness/version'
2
2
  require_relative 'kicks_liveness/heartbeat'
3
3
  require_relative 'kicks_liveness/attempts'
4
+ require_relative 'kicks_liveness/topology_report'
4
5
  require_relative 'kicks_liveness/configuration'
5
6
  require_relative 'kicks_liveness/registry'
6
7
  require_relative 'kicks_liveness/monitor'
@@ -9,8 +10,8 @@ require_relative 'kicks_liveness/hooks'
9
10
  # Liveness probe for Kicks and Sneakers workers, backed by a tmpfs heartbeat.
10
11
  #
11
12
  # The worker publishes a mark from inside its own process, checking its Bunny
12
- # consumers in memory; the probe reads only the mark's mtime. No Rails, no call
13
- # to the broker.
13
+ # consumers in memory; the probe reads only the container generation and mark
14
+ # mtimes. No Rails, no call to the broker.
14
15
  #
15
16
  # @see file:docs/SETUP.md
16
17
  # @see file:docs/DESIGN.md
@@ -76,7 +77,8 @@ module KicksLiveness
76
77
  # @param slot [Integer] supervisor slot of this fork
77
78
  # @param processes [Integer] how many forks the probe must wait for
78
79
  # @param consumers [Integer] how many workers must subscribe here
79
- # @return [Thread, nil] nil when disabled by configuration
80
+ # @return [Thread, nil] nil when disabled by configuration, or when the PID
81
+ # topology is proven unsupported
80
82
  # @api private
81
83
  def start!(slot:, processes:, consumers:)
82
84
  return unless config.enabled?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kicks_liveness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - PoroshkinaVV
@@ -11,12 +11,8 @@ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: |
13
13
  A liveness probe for RabbitMQ worker pods that loads no Rails and never talks
14
- to the broker. The worker publishes a heartbeat to tmpfs from inside its own
15
- process, checking its Bunny consumers in memory; the probe only reads the
16
- file's mtime and runs as `bundle exec kicks-liveness`. It loads no application
17
- code; aside from Bundler and the interpreter it loads only the gem's small
18
- probe files, while the state itself comes from tmpfs. A broker hiccup cannot
19
- restart every replica at once.
14
+ to the broker. Workers check their Bunny consumers in memory and publish a
15
+ heartbeat to tmpfs; the probe only reads that heartbeat.
20
16
  email:
21
17
  - lera.poroshkina@mail.ru
22
18
  executables:
@@ -43,13 +39,14 @@ files:
43
39
  - lib/kicks_liveness/probe.rb
44
40
  - lib/kicks_liveness/railtie.rb
45
41
  - lib/kicks_liveness/registry.rb
42
+ - lib/kicks_liveness/topology_report.rb
46
43
  - lib/kicks_liveness/version.rb
47
44
  homepage: https://github.com/PoroshkinaVV/kicks_liveness
48
45
  licenses:
49
46
  - MIT
50
47
  metadata:
51
48
  source_code_uri: https://github.com/PoroshkinaVV/kicks_liveness
52
- documentation_uri: https://rubydoc.info/gems/kicks_liveness/0.1.1
49
+ documentation_uri: https://rubydoc.info/gems/kicks_liveness/0.1.3
53
50
  changelog_uri: https://github.com/PoroshkinaVV/kicks_liveness/blob/main/CHANGELOG.md
54
51
  bug_tracker_uri: https://github.com/PoroshkinaVV/kicks_liveness/issues
55
52
  rubygems_mfa_required: 'true'