lusnoc 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|