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 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
+
@@ -0,0 +1,12 @@
1
+ require 'timeout'
2
+
3
+ module Lusnoc
4
+
5
+ class Error < RuntimeError; end
6
+
7
+ class TimeoutError < Error; end
8
+
9
+ class ExpiredError < Error; end
10
+
11
+ end
12
+
@@ -0,0 +1,14 @@
1
+ module Lusnoc
2
+ module Helper
3
+
4
+ def logger
5
+ Lusnoc.configuration.logger
6
+ end
7
+
8
+ def build_url(path)
9
+ Lusnoc.configuration.url + path
10
+ end
11
+
12
+ end
13
+ end
14
+
@@ -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,6 @@
1
+ module Lusnoc
2
+
3
+ VERSION = '0.0.1'.freeze
4
+
5
+ end
6
+
@@ -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: []