kubernetes_leader_election 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/lib/kubernetes_leader_election.rb +123 -0
- data/lib/kubernetes_leader_election/version.rb +4 -0
- metadata +59 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8b058cfd69bc5948abfc193c5486b6a8292c10d3364bfd0ebec98faf37b34fff
|
4
|
+
data.tar.gz: 3d800cc726a136c6664ff2aac8e2f1c2005159c6532929f3208590b9fa03fb8c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6294c95ea8b4f283833fb398bf1119d42557341c4fbb2ce7612f2ed20906020bddf200bd0a6d8203fc3d0a7d57a068f1411f5979b0ee33c22f1fbb3ab91752a9
|
7
|
+
data.tar.gz: 4dbd4d8ac315543be78d73dcd156d926e7c0fd775cc637c5871a3b4e5a602b0ff81ad697c6a60343f66295669699696bb56fc799721801d2b5c578e0fbd954e3
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (C) 2013 Michael Grosser <michael@grosser.it>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'time'
|
3
|
+
require 'openssl'
|
4
|
+
require 'timeout'
|
5
|
+
require 'kubeclient'
|
6
|
+
|
7
|
+
class KubernetesLeaderElection
|
8
|
+
ALREADY_EXISTS_CODE = 409
|
9
|
+
FAILED_KUBERNETES_REQUEST =
|
10
|
+
[Timeout::Error, OpenSSL::SSL::SSLError, Kubeclient::HttpError, SystemCallError, HTTP::ConnectionError].freeze
|
11
|
+
|
12
|
+
def initialize(name, kubeclient, statsd:, logger:, interval: 30)
|
13
|
+
@name = name
|
14
|
+
@kubeclient = kubeclient
|
15
|
+
@statsd = statsd
|
16
|
+
@logger = logger
|
17
|
+
@interval = interval
|
18
|
+
end
|
19
|
+
|
20
|
+
# not using `call` since we never want to be restarted
|
21
|
+
def become_leader_for_life
|
22
|
+
@logger.info message: "trying to become leader ... if both pods show this, delete the #{@name} lease"
|
23
|
+
loop do
|
24
|
+
break if become_leader
|
25
|
+
sleep @interval
|
26
|
+
end
|
27
|
+
yield # signal we are leader, but keep reporting
|
28
|
+
loop do
|
29
|
+
@statsd.increment('leader_running') # we monitor this to make sure it's always exactly 1
|
30
|
+
sleep @interval
|
31
|
+
signal_alive
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# show that we are alive or crash because we cannot reach the api (split-brain az)
|
38
|
+
def signal_alive
|
39
|
+
with_retries(*FAILED_KUBERNETES_REQUEST, times: 3) do
|
40
|
+
patch = { spec: { renewTime: microtime } }
|
41
|
+
reply = @kubeclient.patch_entity(
|
42
|
+
"leases", @name, patch, 'strategic-merge-patch', ENV.fetch("POD_NAMESPACE")
|
43
|
+
)
|
44
|
+
|
45
|
+
current_leader = reply.dig(:metadata, :ownerReferences, 0, :name)
|
46
|
+
raise "Lost leadership to #{current_leader}" if current_leader != ENV.fetch("POD_NAME")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# kubernetes needs exactly this format or it blows up
|
51
|
+
def microtime
|
52
|
+
Time.now.strftime('%FT%T.000000Z')
|
53
|
+
end
|
54
|
+
|
55
|
+
def alive?(lease)
|
56
|
+
Time.parse(lease.dig(:spec, :renewTime)) > Time.now - (2 * @interval)
|
57
|
+
end
|
58
|
+
|
59
|
+
# everyone tries to create the same leases, who succeeds is the owner,
|
60
|
+
# leases is auto-deleted by GC when owner is deleted
|
61
|
+
# same logic lives in kube-service-watcher & kube-stats
|
62
|
+
def become_leader
|
63
|
+
namespace = ENV.fetch("POD_NAMESPACE")
|
64
|
+
# retry request on regular api errors
|
65
|
+
reraise = ->(e) { e.is_a?(Kubeclient::HttpError) && e.error_code == ALREADY_EXISTS_CODE }
|
66
|
+
|
67
|
+
with_retries(*FAILED_KUBERNETES_REQUEST, reraise: reraise, times: 3) do
|
68
|
+
@kubeclient.create_entity(
|
69
|
+
"Lease",
|
70
|
+
"leases",
|
71
|
+
metadata: {
|
72
|
+
name: @name,
|
73
|
+
namespace: namespace,
|
74
|
+
ownerReferences: [{
|
75
|
+
apiVersion: "v1",
|
76
|
+
kind: "Pod",
|
77
|
+
name: ENV.fetch("POD_NAME"),
|
78
|
+
uid: ENV.fetch("POD_UID")
|
79
|
+
}]
|
80
|
+
},
|
81
|
+
spec: {
|
82
|
+
acquireTime: microtime,
|
83
|
+
holderIdentity: ENV.fetch("POD_NAME"), # shown in `kubectl get lease`
|
84
|
+
leaseDurationSeconds: @interval * 2,
|
85
|
+
leaseTransitions: 0, # will never change since we delete the lease
|
86
|
+
renewTime: microtime
|
87
|
+
}
|
88
|
+
)
|
89
|
+
end
|
90
|
+
@logger.info message: "became leader"
|
91
|
+
true # I'm the leader now
|
92
|
+
rescue Kubeclient::HttpError => e
|
93
|
+
raise e unless e.error_code == ALREADY_EXISTS_CODE # lease already exists
|
94
|
+
|
95
|
+
lease = with_retries(*FAILED_KUBERNETES_REQUEST, times: 3) do
|
96
|
+
@kubeclient.get_entity("leases", @name, namespace)
|
97
|
+
end
|
98
|
+
leader = lease.dig(:metadata, :ownerReferences, 0, :name)
|
99
|
+
if leader == ENV.fetch("POD_NAME")
|
100
|
+
@logger.info message: "still leader"
|
101
|
+
true # I restarted and am still the leader
|
102
|
+
elsif !alive?(lease)
|
103
|
+
@logger.info message: "deleting stale lease"
|
104
|
+
with_retries(*FAILED_KUBERNETES_REQUEST, times: 3) do
|
105
|
+
@kubeclient.delete_entity("leases", @name, namespace)
|
106
|
+
end
|
107
|
+
false
|
108
|
+
else
|
109
|
+
false # leader is still alive ... not logging to avoid repetitive noise
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def with_retries(*errors, times:, reraise: nil, backoff: [0.1, 0.5, 1])
|
114
|
+
yield
|
115
|
+
rescue *errors => e
|
116
|
+
retries ||= -1
|
117
|
+
retries += 1
|
118
|
+
raise if retries >= times || reraise&.call(e)
|
119
|
+
@logger.warn message: "Retryable error", type: e.class.to_s, retries: times - retries
|
120
|
+
sleep backoff[retries] || backoff.last
|
121
|
+
retry
|
122
|
+
end
|
123
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kubernetes_leader_election
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Grosser
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-08-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: kubeclient
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description:
|
28
|
+
email: michael@grosser.it
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- MIT-LICENSE
|
34
|
+
- lib/kubernetes_leader_election.rb
|
35
|
+
- lib/kubernetes_leader_election/version.rb
|
36
|
+
homepage: https://github.com/grosser/kubernetes_leader_election
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata: {}
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 2.6.0
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubygems_version: 3.2.16
|
56
|
+
signing_key:
|
57
|
+
specification_version: 4
|
58
|
+
summary: Elect a kubernetes leader using leases for ruby
|
59
|
+
test_files: []
|