keepit 0.1.0
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/.drone.yml +16 -0
- data/.gitignore +12 -0
- data/.rspec +4 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +4 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/dip.yml +38 -0
- data/docker-compose.development.yml +13 -0
- data/docker-compose.drone.yml +7 -0
- data/docker-compose.yml +9 -0
- data/keepit.gemspec +30 -0
- data/lib/keepit.rb +17 -0
- data/lib/keepit/decaying.rb +34 -0
- data/lib/keepit/guard.rb +66 -0
- data/lib/keepit/locker.rb +35 -0
- data/lib/keepit/transient_store.rb +53 -0
- data/lib/keepit/version.rb +3 -0
- metadata +187 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cc0d1dfa47aa651e39fd114b33e6065f02809111
|
4
|
+
data.tar.gz: 7a7a3ce7e46f67e824435d28a3d57c794cb78a6f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2e5844e3312176679e74388a65a30dd4ea4324fc03cd3d1c873e52d4cba9b7ee6a482cbafc2e6f24db840ae564beda3dd2fa5f59ba7745a0c9885578b51d2802
|
7
|
+
data.tar.gz: cb4c0ab8c1da0a147a358d62d4bfb6ce0fb5a97886115197ba5eee4faefd442fcb890fc06c458eaf3bd910387365429bcad64c94819d8d141111eba4b528e264
|
data/.drone.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
build:
|
2
|
+
test:
|
3
|
+
image: abakpress/dind-testing
|
4
|
+
pull: true
|
5
|
+
privileged: true
|
6
|
+
volumes:
|
7
|
+
- /home/data/drone/images:/images
|
8
|
+
- /home/data/drone/gems:/bundle
|
9
|
+
environment:
|
10
|
+
- COMPOSE_FILE_EXT=drone
|
11
|
+
- RUBY_IMAGE_TAG=2.2-latest
|
12
|
+
commands:
|
13
|
+
- wrapdocker docker -v
|
14
|
+
- fetch-images --image abakpress/ruby-app:$RUBY_IMAGE_TAG
|
15
|
+
- dip provision
|
16
|
+
- dip rspec
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
File without changes
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Keepit
|
2
|
+
|
3
|
+
Tools for safe interacting with external api services.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'keepit'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install keepit
|
20
|
+
|
21
|
+
## Development
|
22
|
+
|
23
|
+
After checking out the repo, run `dip provision` to install dependencies. Then, run `dip rspec` to run the tests.
|
24
|
+
|
25
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
26
|
+
|
27
|
+
## Contributing
|
28
|
+
|
29
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/abak-press/keepit.
|
data/Rakefile
ADDED
data/dip.yml
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
version: '1'
|
2
|
+
|
3
|
+
environment:
|
4
|
+
DOCKER_RUBY_VERSION: 2.2
|
5
|
+
RUBY_IMAGE_TAG: 2.2-latest
|
6
|
+
COMPOSE_FILE_EXT: development
|
7
|
+
RAILS_ENV: test
|
8
|
+
APRESS_GEMS_CREDENTIALS: ""
|
9
|
+
|
10
|
+
compose:
|
11
|
+
files:
|
12
|
+
- docker-compose.yml
|
13
|
+
- docker-compose.${COMPOSE_FILE_EXT}.yml
|
14
|
+
|
15
|
+
interaction:
|
16
|
+
sh:
|
17
|
+
service: app
|
18
|
+
|
19
|
+
irb:
|
20
|
+
service: app
|
21
|
+
command: irb
|
22
|
+
|
23
|
+
bundle:
|
24
|
+
service: app
|
25
|
+
command: bundle
|
26
|
+
|
27
|
+
rspec:
|
28
|
+
service: app
|
29
|
+
command: bundle exec rspec
|
30
|
+
|
31
|
+
clean:
|
32
|
+
service: app
|
33
|
+
command: rm -f Gemfile.lock
|
34
|
+
|
35
|
+
provision:
|
36
|
+
- docker volume create --name bundler_data
|
37
|
+
- dip clean
|
38
|
+
- dip bundle install
|
data/docker-compose.yml
ADDED
data/keepit.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "keepit/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "keepit"
|
7
|
+
spec.version = Keepit::VERSION
|
8
|
+
spec.authors = ["Michail Merkushin"]
|
9
|
+
spec.email = ["merkushin.m.s@gmail.com"]
|
10
|
+
spec.summary = "Classes for protection"
|
11
|
+
spec.homepage = "https://github.com/abak-press/keepit"
|
12
|
+
|
13
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
14
|
+
f.match(%r{^(test|spec|features)/})
|
15
|
+
end
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency "redis"
|
21
|
+
spec.add_runtime_dependency "dry-configurable"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
26
|
+
spec.add_development_dependency "mock_redis"
|
27
|
+
spec.add_development_dependency "timecop"
|
28
|
+
spec.add_development_dependency "pry-byebug"
|
29
|
+
spec.add_development_dependency "test-unit"
|
30
|
+
end
|
data/lib/keepit.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "redis"
|
2
|
+
require "dry/configurable"
|
3
|
+
require "keepit/decaying"
|
4
|
+
require "keepit/transient_store"
|
5
|
+
require "keepit/locker"
|
6
|
+
require "keepit/guard"
|
7
|
+
require "keepit/version"
|
8
|
+
|
9
|
+
module Keepit
|
10
|
+
extend Dry::Configurable
|
11
|
+
|
12
|
+
setting :redis
|
13
|
+
|
14
|
+
def self.redis
|
15
|
+
config.redis || ::Redis.current
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Keepit
|
2
|
+
# A float value which decays exponentially toward 0 over time.
|
3
|
+
class Decaying
|
4
|
+
attr_accessor :e
|
5
|
+
attr_accessor :p
|
6
|
+
|
7
|
+
# opts - Hash
|
8
|
+
# :p - Float (0.0) The initial value
|
9
|
+
# :e - Float (Math::E) Exponent base
|
10
|
+
# :r - Float (Math.log(0.5) / 10) Timescale factor - defaulting to decay 50% every 10 seconds
|
11
|
+
def initialize(opts = {})
|
12
|
+
@p = opts[:p] || 0.0
|
13
|
+
@e = opts[:e] || Math::E
|
14
|
+
@r = opts[:r] || Math.log(0.5) / 10
|
15
|
+
@t0 = Time.now.to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
# Add to current value
|
19
|
+
#
|
20
|
+
# d - Float value to add
|
21
|
+
def <<(d)
|
22
|
+
@p = value + d
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns Float the current value (adjusted for the time decay)
|
26
|
+
def value
|
27
|
+
return 0.0 unless @p > 0
|
28
|
+
now = Time.now.to_i
|
29
|
+
dt = now - @t0
|
30
|
+
@t0 = now
|
31
|
+
@p *= @e**(@r * dt)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/keepit/guard.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Базовый класс защитника от ошибок при обращении к сторонним сервисам.
|
2
|
+
#
|
3
|
+
# Основной принцип работы:
|
4
|
+
#
|
5
|
+
# Каждый сервис-клиент реализует свой класс защитника со списком возможных исключений.
|
6
|
+
# Если сторонний сервис возвращает определнное кол-во ошибок, которое больше некого порога,
|
7
|
+
# то защитник перестает давать общаться с ним на некоторое время.
|
8
|
+
# До преодоления порога мы сообщаем об ошибках в NewRelic и письмом проектому менеджеру.
|
9
|
+
# Также защитник не дает общаться с сервисом, если он находится на тех. обслуживании.
|
10
|
+
#
|
11
|
+
# Examples:
|
12
|
+
#
|
13
|
+
# Keepit::Guard.config.error_notificator = ->(resource, error) { ErrorMailer.report(resource, error) }
|
14
|
+
#
|
15
|
+
# module BarmenClient
|
16
|
+
# class Guard < ::Keepit::Guard
|
17
|
+
# config.resource = "barmen".freeze
|
18
|
+
# config.rescue_errors = [ActiveResource::ConnectionError]
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# BarmenClient::Guard.wrap { BarmenClient::Banner.find(...) }
|
23
|
+
#
|
24
|
+
module Keepit
|
25
|
+
class Guard
|
26
|
+
extend Dry::Configurable
|
27
|
+
|
28
|
+
setting :resource, reader: true
|
29
|
+
setting :rescue_errors, [StandardError], reader: true
|
30
|
+
setting :max_error_rate, 10, reader: true
|
31
|
+
setting :error_notificator, reader: true
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def wrap
|
35
|
+
return nil unless fine?
|
36
|
+
yield
|
37
|
+
rescue *rescue_errors => error
|
38
|
+
error_rates[resource] << 1
|
39
|
+
error_notificator.call(resource, error)
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def reset
|
44
|
+
error_rates.clear
|
45
|
+
end
|
46
|
+
|
47
|
+
def fine?
|
48
|
+
!error_rate_exceeded? && !resource_locked?
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def error_rates
|
54
|
+
@error_rates ||= Hash.new { |hash, key| hash[key] = ::Keepit::Decaying.new }
|
55
|
+
end
|
56
|
+
|
57
|
+
def error_rate_exceeded?
|
58
|
+
error_rates[resource].value >= (max_error_rate || DEFAULT_MAX_ERROR_RATE)
|
59
|
+
end
|
60
|
+
|
61
|
+
def resource_locked?
|
62
|
+
::Keepit::Locker.locked?(resource, check_global: false)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Keepit
|
2
|
+
module Locker
|
3
|
+
GLOBAL = "global".freeze
|
4
|
+
KEY = "keepit:locks".freeze
|
5
|
+
EXPIRES_IN = 60
|
6
|
+
|
7
|
+
def self.lock(resources, blocking = false)
|
8
|
+
result = ::Keepit.redis.sadd(KEY, resources)
|
9
|
+
sleep(EXPIRES_IN) if blocking
|
10
|
+
|
11
|
+
result
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.unlock(resources, blocking = false)
|
15
|
+
result = ::Keepit.redis.srem(KEY, resources)
|
16
|
+
sleep(EXPIRES_IN) if blocking
|
17
|
+
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.locked?(resource, check_global: true)
|
22
|
+
if check_global
|
23
|
+
locked_resources.any? { |r| r == resource || r == GLOBAL }
|
24
|
+
else
|
25
|
+
locked_resources.any? { |r| r == resource }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.locked_resources
|
30
|
+
::Keepit::TransientStore.fetch(:locks, expires_in: EXPIRES_IN) do
|
31
|
+
Array(::Keepit.redis.smembers(KEY))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Keepit
|
2
|
+
module TransientStore
|
3
|
+
Value = Struct.new(:value, :expired_at) do
|
4
|
+
def expired?
|
5
|
+
expired_at < Time.now.to_i
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.clear
|
10
|
+
store.clear
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.delete(key)
|
14
|
+
item = store.delete(key)
|
15
|
+
item.value if item
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.empty?
|
19
|
+
store.empty?
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.exist?(key)
|
23
|
+
return true if store.key?(key) && !store[key].expired?
|
24
|
+
|
25
|
+
delete(key)
|
26
|
+
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.fetch(key, expires_in:, &block)
|
31
|
+
if exist?(key)
|
32
|
+
store[key].value
|
33
|
+
else
|
34
|
+
value = yield
|
35
|
+
write(key, value, expires_in: expires_in)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.read(key)
|
40
|
+
store[key].value if exist?(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.store
|
44
|
+
Thread.current[:keepit_transient_store] ||= {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.write(key, value, expires_in:)
|
48
|
+
store[key] = Value.new(value, Time.now.to_i + expires_in.to_i)
|
49
|
+
|
50
|
+
value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: keepit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michail Merkushin
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
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
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dry-configurable
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.7'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.7'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: mock_redis
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: timecop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: pry-byebug
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: test-unit
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
- merkushin.m.s@gmail.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".drone.yml"
|
147
|
+
- ".gitignore"
|
148
|
+
- ".rspec"
|
149
|
+
- CHANGELOG.md
|
150
|
+
- Gemfile
|
151
|
+
- README.md
|
152
|
+
- Rakefile
|
153
|
+
- dip.yml
|
154
|
+
- docker-compose.development.yml
|
155
|
+
- docker-compose.drone.yml
|
156
|
+
- docker-compose.yml
|
157
|
+
- keepit.gemspec
|
158
|
+
- lib/keepit.rb
|
159
|
+
- lib/keepit/decaying.rb
|
160
|
+
- lib/keepit/guard.rb
|
161
|
+
- lib/keepit/locker.rb
|
162
|
+
- lib/keepit/transient_store.rb
|
163
|
+
- lib/keepit/version.rb
|
164
|
+
homepage: https://github.com/abak-press/keepit
|
165
|
+
licenses: []
|
166
|
+
metadata: {}
|
167
|
+
post_install_message:
|
168
|
+
rdoc_options: []
|
169
|
+
require_paths:
|
170
|
+
- lib
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
requirements: []
|
182
|
+
rubyforge_project:
|
183
|
+
rubygems_version: 2.6.8
|
184
|
+
signing_key:
|
185
|
+
specification_version: 4
|
186
|
+
summary: Classes for protection
|
187
|
+
test_files: []
|