lusnoc 0.0.1
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 +7 -0
- data/lib/lusnoc/configuration.rb +20 -0
- data/lib/lusnoc/exceptions.rb +12 -0
- data/lib/lusnoc/helper.rb +14 -0
- data/lib/lusnoc/mutex.rb +97 -0
- data/lib/lusnoc/session.rb +105 -0
- data/lib/lusnoc/timeouter.rb +51 -0
- data/lib/lusnoc/version.rb +6 -0
- data/lib/lusnoc/watcher.rb +44 -0
- data/lib/lusnoc.rb +71 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 16c854beb66cb33dbc4cb2ee0026f4981de6f95a1ce3c86e3fec22a5c7e7f382
|
4
|
+
data.tar.gz: 6f07727a72751aab2640714fe40c1b252d75fdc79388c1b20aecde9ba84e6396
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6d0bbb37004b463725e8f0261ca239c7f9ae146655a2e28cf8510989c70a35e6c8c03c8a1370645098183f263f5dcecaf8e1b8d1e570876814a25842f0077f49
|
7
|
+
data.tar.gz: 9ccba26ba0be610089dc8e0a24b4ba58846a3d8d41f11a88f4272592063602e35f4fb554b97e2fff42b80e842ee1ebf6fea417bbdbf1fb6e283c4002e647f64b
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Lusnoc
|
4
|
+
# Methods for configuring Lusnoc
|
5
|
+
class Configuration
|
6
|
+
|
7
|
+
attr_accessor :url, :acl_token, :logger
|
8
|
+
|
9
|
+
# Override defaults for configuration
|
10
|
+
# @param url [String] consul's connection URL
|
11
|
+
# @param acl_token [String] a connection token used when making requests to consul
|
12
|
+
def initialize(url = 'http://localhost:8500', acl_token = nil)
|
13
|
+
@url = url
|
14
|
+
@acl_token = acl_token
|
15
|
+
@logger = Logger.new(STDOUT, level: Logger::INFO, progname: 'Lusnoc')
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
data/lib/lusnoc/mutex.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'lusnoc/session'
|
4
|
+
|
5
|
+
module Lusnoc
|
6
|
+
class Mutex
|
7
|
+
|
8
|
+
include Helper
|
9
|
+
attr_reader :key, :value, :owner
|
10
|
+
|
11
|
+
def initialize(key, value = Socket.gethostname, ttl: 20)
|
12
|
+
@key = key
|
13
|
+
@value = value
|
14
|
+
@ttl = ttl
|
15
|
+
end
|
16
|
+
|
17
|
+
def locked?
|
18
|
+
!!owner
|
19
|
+
end
|
20
|
+
|
21
|
+
def owned?
|
22
|
+
owner == Thread.current
|
23
|
+
end
|
24
|
+
|
25
|
+
def session_id
|
26
|
+
@session&.id
|
27
|
+
end
|
28
|
+
|
29
|
+
[:time_to_expiration, :need_renew?, :ttl, :expired?, :live?, :live!, :renew].each do |m|
|
30
|
+
define_method(m) {@session&.public_send(m)}
|
31
|
+
end
|
32
|
+
|
33
|
+
def synchronize(timeout: 0, &block)
|
34
|
+
timeouter = Timeouter.new(timeout,
|
35
|
+
exception_class: TimeoutError,
|
36
|
+
exception_message: 'mutex acquisition expired')
|
37
|
+
|
38
|
+
Session.new("mutex_session/#{key}", ttl: @ttl) do |session|
|
39
|
+
@session = session
|
40
|
+
session.on_session_die do
|
41
|
+
@owner = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
return acquisition_loop! key, session, value, timeouter, &block
|
45
|
+
ensure
|
46
|
+
release(key, session.id, timeout: 2) rescue nil
|
47
|
+
logger.info("Lock #{key} released for session #{session.name}[#{session.id}]")
|
48
|
+
@owner = nil
|
49
|
+
@session = nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def acquire(key, session, value)
|
56
|
+
resp = Lusnoc.http_put(build_url("/v1/kv/#{key}?acquire=#{session.id}"), value, timeout: 1)
|
57
|
+
return false if resp.body.chomp != 'true'
|
58
|
+
|
59
|
+
@owner = Thread.current
|
60
|
+
logger.info("Lock #{key} acquired for session #{session.name}[#{session.id}]")
|
61
|
+
renew
|
62
|
+
true
|
63
|
+
end
|
64
|
+
|
65
|
+
def release(key, session)
|
66
|
+
Lusnoc.http_put(build_url("/v1/kv/#{key}?release=#{session.id}"), timeout: 1)
|
67
|
+
end
|
68
|
+
|
69
|
+
def acquisition_loop!(key, session, value, timeouter)
|
70
|
+
return yield(self) if acquire(key, session, value)
|
71
|
+
|
72
|
+
logger.debug("Start #{key} acquisition loop for session #{session.name}[#{session.id}]")
|
73
|
+
timeouter.loop! do
|
74
|
+
session.live!(TimeoutError)
|
75
|
+
wait_for_key_released(key, timeouter.left)
|
76
|
+
|
77
|
+
return yield(self) if acquire(key, session, value)
|
78
|
+
|
79
|
+
logger.debug("Lock #{key} acquisition failed for session #{session.name}[#{session.id}]")
|
80
|
+
sleep 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def wait_for_key_released(key, timeout = nil)
|
85
|
+
logger.debug "Waiting for key #{key} to be fre of any session"
|
86
|
+
Lusnoc::Watcher.new(build_url("/v1/kv/#{key}"),
|
87
|
+
timeout: timeout,
|
88
|
+
exception_class: TimeoutError,
|
89
|
+
exception_message: 'mutex acquisition expired').run do |body|
|
90
|
+
result = JSON.parse(body.empty? ? '[{}]' : body)
|
91
|
+
return true if result.first['Session'].nil?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'lusnoc/watcher'
|
2
|
+
|
3
|
+
module Lusnoc
|
4
|
+
class Session
|
5
|
+
|
6
|
+
include Helper
|
7
|
+
|
8
|
+
attr_reader :id, :name, :ttl, :live, :expired_at
|
9
|
+
|
10
|
+
def initialize(name, ttl: 20)
|
11
|
+
@name = name
|
12
|
+
@ttl = ttl
|
13
|
+
|
14
|
+
@id = create_session(name, ttl)
|
15
|
+
yield(self)
|
16
|
+
ensure
|
17
|
+
destroy_session(@id)
|
18
|
+
end
|
19
|
+
|
20
|
+
def expired?
|
21
|
+
!live?
|
22
|
+
end
|
23
|
+
|
24
|
+
def time_to_expiration
|
25
|
+
@expired_at && @expired_at - Time.now
|
26
|
+
end
|
27
|
+
|
28
|
+
def need_renew?
|
29
|
+
time_to_expiration && time_to_expiration < (@ttl / 2.0)
|
30
|
+
end
|
31
|
+
|
32
|
+
def live?
|
33
|
+
@live
|
34
|
+
end
|
35
|
+
|
36
|
+
def live!(exception_class = ExpiredError)
|
37
|
+
live? || (raise exception_class.new("Session #{id} expired"))
|
38
|
+
end
|
39
|
+
|
40
|
+
def renew
|
41
|
+
live!
|
42
|
+
Lusnoc.http_put(build_url("/v1/session/renew/#{@id}"), nil, timeout: 1)
|
43
|
+
@expired_at = Time.now + ttl
|
44
|
+
logger.info "Session renewed: #{name}[#{@id}]. Next expiration: #{@expired_at}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_session_die(&block)
|
48
|
+
@session_die_cb = block
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def create_session(name, ttl)
|
54
|
+
resp = Lusnoc.http_put(build_url('/v1/session/create'),
|
55
|
+
{ Name: name, TTL: "#{ttl}s", LockDelay: '5s' },
|
56
|
+
{ timeout: 1 })
|
57
|
+
session_id = JSON.parse(resp.body)['ID']
|
58
|
+
@expired_at = Time.now + ttl
|
59
|
+
logger.info "Session created: #{name}[#{session_id}]. TTL:#{ttl}s. Next expiration: #{@expired_at}"
|
60
|
+
@live = true
|
61
|
+
@th = start_watch_thread(session_id)
|
62
|
+
session_id
|
63
|
+
end
|
64
|
+
|
65
|
+
def destroy_session(session_id)
|
66
|
+
@th.kill rescue nil
|
67
|
+
Lusnoc.http_put(build_url("/v1/session/destroy/#{session_id}"),
|
68
|
+
nil,
|
69
|
+
timeout: 1) rescue nil
|
70
|
+
logger.info "Session destroyed: #{name}[#{session_id}]"
|
71
|
+
@live = false
|
72
|
+
@expired_at = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def start_watch_thread(session_id)
|
76
|
+
Thread.new do
|
77
|
+
logger.debug "Guard thread for Session #{name}[#{session_id}] started"
|
78
|
+
|
79
|
+
if wait_forever_for_session_gone(session_id)
|
80
|
+
logger.error "Session #{name}[#{session_id}] is gone"
|
81
|
+
@live = false
|
82
|
+
@expired_at = nil
|
83
|
+
@session_die_cb&.call(self)
|
84
|
+
else
|
85
|
+
logger.unknown 'Something is wrong with thread logic'
|
86
|
+
end
|
87
|
+
ensure
|
88
|
+
logger.debug "Guard thread for Session #{name}[#{session_id}] finihsed"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def wait_forever_for_session_gone(session_id)
|
93
|
+
Lusnoc::Watcher.new(build_url("/v1/session/info/#{session_id}"), timeout: 0).run do |body|
|
94
|
+
true if JSON.parse(body).empty?
|
95
|
+
end
|
96
|
+
rescue StandardError => e
|
97
|
+
logger.error "Session #{name}[#{session_id}] watch exception: #{e.inspect}"
|
98
|
+
logger.error e.backtrace.join("\n")
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'lusnoc/exceptions'
|
2
|
+
|
3
|
+
module Lusnoc
|
4
|
+
class Timeouter
|
5
|
+
|
6
|
+
attr_reader :exhausted_at, :started_at
|
7
|
+
|
8
|
+
def initialize(timeout = 0,
|
9
|
+
exception_class: TimeoutError,
|
10
|
+
exception_message: 'execution expired')
|
11
|
+
timeout ||= 0
|
12
|
+
timeout = [timeout, 0].max
|
13
|
+
|
14
|
+
@default_exception_class = exception_class
|
15
|
+
@default_exception_message = exception_message
|
16
|
+
|
17
|
+
@started_at = Time.now.to_f
|
18
|
+
@exhausted_at = timeout > 0 ? @started_at + timeout : nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.timeout(timeout = 0)
|
22
|
+
self.new(timeout)
|
23
|
+
end
|
24
|
+
|
25
|
+
def elapsed
|
26
|
+
Time.now.to_f - @started_at
|
27
|
+
end
|
28
|
+
|
29
|
+
def left
|
30
|
+
@exhausted_at && [@exhausted_at - Time.now.to_f, 0].max
|
31
|
+
end
|
32
|
+
|
33
|
+
def check
|
34
|
+
!@exhausted_at || (@exhausted_at > Time.now.to_f)
|
35
|
+
end
|
36
|
+
|
37
|
+
def check!(exception_class = @default_exception_class)
|
38
|
+
check || (raise exception_class.new(@default_exception_message))
|
39
|
+
end
|
40
|
+
|
41
|
+
def loop
|
42
|
+
yield(self) while self.check
|
43
|
+
end
|
44
|
+
|
45
|
+
def loop!(exception_class = @default_exception_class)
|
46
|
+
yield(self) while self.check!(exception_class)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'lusnoc/timeouter'
|
2
|
+
require 'lusnoc/helper'
|
3
|
+
|
4
|
+
module Lusnoc
|
5
|
+
class Watcher
|
6
|
+
|
7
|
+
include Helper
|
8
|
+
|
9
|
+
def initialize(base_url,
|
10
|
+
timeout: 0,
|
11
|
+
exception_class: TimeoutError,
|
12
|
+
exception_message: 'watch timeout')
|
13
|
+
@base_url = base_url
|
14
|
+
@timeout = timeout
|
15
|
+
@exception_class = exception_class
|
16
|
+
@exception_message = exception_message
|
17
|
+
end
|
18
|
+
|
19
|
+
# run Consul blocking request in a loop with timeout support.
|
20
|
+
# break condition yielded by block call with response body
|
21
|
+
def run
|
22
|
+
logger.debug "Watch #{@base_url} with #{@timeout.inspect} timeout"
|
23
|
+
last_x_consul_index = 1
|
24
|
+
|
25
|
+
Timeouter.new(@timeout,
|
26
|
+
exception_class: @exception_class,
|
27
|
+
exception_message: @exception_message).loop! do |timeouter|
|
28
|
+
wait_condition = timeouter.left ? "&wait=#{timeouter.left.to_i}s" : ''
|
29
|
+
url = "#{@base_url}?index=#{last_x_consul_index}#{wait_condition}"
|
30
|
+
|
31
|
+
resp = Lusnoc.http_get(url, timeout: timeouter.left)
|
32
|
+
return true if yield(resp.body)
|
33
|
+
|
34
|
+
logger.debug "Watch #{@base_url} response: #{resp.body}"
|
35
|
+
|
36
|
+
index = [Integer(resp['x-consul-index']), 1].max
|
37
|
+
last_x_consul_index = (index < last_x_consul_index ? 1 : index)
|
38
|
+
sleep 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
data/lib/lusnoc.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'lusnoc/configuration'
|
5
|
+
require 'lusnoc/session'
|
6
|
+
require 'lusnoc/mutex'
|
7
|
+
|
8
|
+
|
9
|
+
module Lusnoc
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
attr_accessor :configuration
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
self.configuration ||= Lusnoc::Configuration.new
|
18
|
+
|
19
|
+
class << self
|
20
|
+
|
21
|
+
def configure
|
22
|
+
self.configuration ||= Lusnoc::Configuration.new
|
23
|
+
yield(configuration)
|
24
|
+
end
|
25
|
+
|
26
|
+
def http_get(url, timeout: 1)
|
27
|
+
uri = URI(url)
|
28
|
+
|
29
|
+
with_http(uri, timeout: timeout) do |http|
|
30
|
+
req = Net::HTTP::Get.new(uri)
|
31
|
+
|
32
|
+
# configure http and request before send
|
33
|
+
yield(http, req) if block_given?
|
34
|
+
http.request(req)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def http_put(url, value = nil, timeout: 1)
|
39
|
+
uri = URI(url)
|
40
|
+
data = value.is_a?(String) ? value : JSON.generate(value) unless value.nil?
|
41
|
+
|
42
|
+
with_http(uri, timeout: timeout) do |http|
|
43
|
+
req = Net::HTTP::Put.new(uri).tap do |r|
|
44
|
+
r.body = data
|
45
|
+
r['Content-Type'] = 'application/json'
|
46
|
+
end
|
47
|
+
|
48
|
+
# configure http and request before send
|
49
|
+
yield(http, req) if block_given?
|
50
|
+
http.request(req)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def with_http(uri, timeout:)
|
57
|
+
Net::HTTP.start(uri.host, uri.port,
|
58
|
+
use_ssl: uri.scheme == 'https',
|
59
|
+
read_timeout: timeout,
|
60
|
+
open_timeout: 1,
|
61
|
+
continue_timeout: 1,
|
62
|
+
write_timeout: 1,
|
63
|
+
max_retries: 0) do |http|
|
64
|
+
yield(http)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lusnoc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Samoilenko Yuri
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-09-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.0.1
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.0'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.0.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rake
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rubocop
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: webmock
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: json
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :runtime
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
description: asd
|
104
|
+
email:
|
105
|
+
- kinnalru@gmail.com
|
106
|
+
executables: []
|
107
|
+
extensions: []
|
108
|
+
extra_rdoc_files: []
|
109
|
+
files:
|
110
|
+
- lib/lusnoc.rb
|
111
|
+
- lib/lusnoc/configuration.rb
|
112
|
+
- lib/lusnoc/exceptions.rb
|
113
|
+
- lib/lusnoc/helper.rb
|
114
|
+
- lib/lusnoc/mutex.rb
|
115
|
+
- lib/lusnoc/session.rb
|
116
|
+
- lib/lusnoc/timeouter.rb
|
117
|
+
- lib/lusnoc/version.rb
|
118
|
+
- lib/lusnoc/watcher.rb
|
119
|
+
homepage: https://github.com/RnD-Soft/lusnoc
|
120
|
+
licenses:
|
121
|
+
- MIT
|
122
|
+
metadata: {}
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
require_paths:
|
126
|
+
- lib
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
requirements: []
|
138
|
+
rubygems_version: 3.0.4
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: asd
|
142
|
+
test_files: []
|