lusnoc 0.0.1

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