async_storage 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ffa57bbe53ad587cdd628ce556aaf679ed9b4ff02f5a9b8c4217be4b6ffa495d
4
+ data.tar.gz: 5557aa324800a20d599a94ddcbf21894f8ffa3ccc97672f7e0329d5283090d84
5
+ SHA512:
6
+ metadata.gz: f6f33f4f1d4728c5933c7b312daa602a14eb2408f40168a8b5a3f490e8853e1b84464cc698fb6a3c6721f4bc0b72fb1dae0c4341bec2c481ce46f1acb5128718
7
+ data.tar.gz: 0d976a57d701639a1e46b3f9d236b3e983ac6a652e96e6ac481475f1293326251e0f4e1045ed945027ed6cd26f552fa9ec5793b2337fa6179ebc380f6fb42651
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .rspec_status
10
+ .env
11
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.0
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in async_storage.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 12.0'
9
+ gem 'rspec', '~> 3.0'
10
+ gem 'connection_pool', '~> 2.2.3'
11
+ gem 'pry', '~> 0.13.1'
12
+ gem 'awesome_print', '~> 1.8.0'
13
+ gem 'dotenv', '~> 2.7.6'
@@ -0,0 +1,50 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ async_storage (0.1.0)
5
+ multi_json (> 0.0.0)
6
+ redis (> 0.0.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ awesome_print (1.8.0)
12
+ coderay (1.1.3)
13
+ connection_pool (2.2.3)
14
+ diff-lcs (1.4.4)
15
+ dotenv (2.7.6)
16
+ method_source (1.0.0)
17
+ multi_json (1.15.0)
18
+ pry (0.13.1)
19
+ coderay (~> 1.1)
20
+ method_source (~> 1.0)
21
+ rake (12.3.3)
22
+ redis (4.2.2)
23
+ rspec (3.10.0)
24
+ rspec-core (~> 3.10.0)
25
+ rspec-expectations (~> 3.10.0)
26
+ rspec-mocks (~> 3.10.0)
27
+ rspec-core (3.10.0)
28
+ rspec-support (~> 3.10.0)
29
+ rspec-expectations (3.10.0)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.10.0)
32
+ rspec-mocks (3.10.0)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.10.0)
35
+ rspec-support (3.10.0)
36
+
37
+ PLATFORMS
38
+ ruby
39
+
40
+ DEPENDENCIES
41
+ async_storage!
42
+ awesome_print (~> 1.8.0)
43
+ connection_pool (~> 2.2.3)
44
+ dotenv (~> 2.7.6)
45
+ pry (~> 0.13.1)
46
+ rake (~> 12.0)
47
+ rspec (~> 3.0)
48
+
49
+ BUNDLED WITH
50
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Marcos G. Zimmermann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,94 @@
1
+ # AsyncStorage
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/async_storage`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'async_storage'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install async_storage
22
+
23
+ ## Usage
24
+
25
+ ** Idea about the API of this gem. Update accordingly before release it
26
+
27
+ Define global configurations
28
+ ```ruby
29
+ # Configurations
30
+ AsyncStorage.configuration do |config|
31
+ config.redis = ConnectionPool.new(size: 10, timeout: 1) do
32
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://0.0.0.0:6379'))
33
+ end
34
+ config.namespace = 'async_storage' # Default to 'async_storage'
35
+ config.expires_in = 3_600 # Default to nil
36
+ end
37
+ ```
38
+
39
+ Useful methods to get, set and check data
40
+
41
+ ```ruby
42
+ # app/resolvers/user_tweets_resolver.rb
43
+ class UserTweetsResolver
44
+ def call(user_id)
45
+ # Return JSON friendly object
46
+ { 'user_id' => user_id, 'tweets' => Twitter::API.tweets(user_id).as_json }
47
+ end
48
+ end
49
+
50
+ AsyncStorage[UserTweetResolver].get('123') # Try to retrieve data. If does not exist enqueue a Background Job and return nil
51
+ AsyncStorage[UserTweetResolver].get!('123') # Try to retrieve data. If does not exist imediate call the Resolver and return data
52
+ AsyncStorage[UserTweetResolver, namespace: current_site.id].get(9) # Create a new Set using site id namepace
53
+ AsyncStorage[UserTweetResolver, expires_in: 60].get(9) # Overwrite global expires_in
54
+ ```
55
+
56
+ ```ruby
57
+ class Site
58
+ # site.cache.user_tweets.get(@user.id)
59
+ def cache
60
+ Cache.new(self.slug)
61
+ end
62
+
63
+ class Cache
64
+ RESOLVERS = {
65
+ user_tweets: UserTweetsResolver,
66
+ }.freeze
67
+
68
+ RESOLVERS.each do |method, resolver|
69
+ define_method method do
70
+ AsyncStorage[resolver, namespace: @namespace]
71
+ end
72
+ end
73
+
74
+ def initialize(namespace)
75
+ @namespace = namespace
76
+ end
77
+ end
78
+ end
79
+ ```
80
+
81
+ ## Development
82
+
83
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
84
+
85
+ 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).
86
+
87
+ ## Contributing
88
+
89
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcosgz/async_storage.
90
+
91
+
92
+ ## License
93
+
94
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/async_storage/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'async_storage'
7
+ spec.version = AsyncStorage::VERSION
8
+ spec.authors = ['Marcos G. Zimmermann']
9
+ spec.email = ['mgzmaster@gmail.com']
10
+
11
+ spec.summary = 'Asynchronous key-value storage system'
12
+ spec.description = 'Asynchronous key-value storage system'
13
+ spec.homepage = 'https://github.com/marcosgz/async_storage'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/marcosgz/async_storage'
19
+ spec.metadata['changelog_uri'] = 'https://github.com/marcosgz/async_storage/blob/master/CHANGELOG.md'
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ spec.add_dependency 'redis', '> 0.0.0'
31
+ spec.add_dependency 'multi_json', '> 0.0.0'
32
+ end
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'dotenv/load'
5
+ require 'pry'
6
+ require 'awesome_print'
7
+ require 'async_storage'
8
+ require 'connection_pool'
9
+
10
+ AsyncStorage.configure do |config|
11
+ config.redis = ConnectionPool.new(size: 2, timeout: 0.5) do
12
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://0.0.0.0:6379'))
13
+ end
14
+ config.expires_in = 120
15
+ end
16
+
17
+ class DummyResolver
18
+ def call(*values); values; end
19
+ end
20
+
21
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async_storage/version'
4
+ require 'async_storage/config'
5
+ require 'async_storage/redis_pool'
6
+ require 'async_storage/json'
7
+ require 'async_storage/repo'
8
+ require 'async_storage/bath_actions'
9
+
10
+ module AsyncStorage
11
+ class Error < StandardError; end
12
+ class InvalidConfig < Error; end
13
+
14
+ module_function
15
+
16
+ def [](klass, **options)
17
+ Repo.new(klass, **options)
18
+ end
19
+
20
+ def config
21
+ @config ||= Config.new
22
+ end
23
+
24
+ def configure(&block)
25
+ return unless block_given?
26
+
27
+ config.instance_eval(&block)
28
+ @redis_pool = nil
29
+ config
30
+ end
31
+
32
+ def redis_pool
33
+ @redis_pool ||= RedisPool.new(config.redis)
34
+ end
35
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsyncStorage
4
+ module_function
5
+
6
+ def flush_all
7
+ keys.inject(0) do |total, (key, cli)|
8
+ total + cli.del(key)
9
+ end
10
+ end
11
+
12
+ def keys
13
+ Enumerator.new do |yielder|
14
+ redis_pool.with do |cli|
15
+ cli.keys("#{config.namespace}:*").each { |key| yielder.yield(key, cli) }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,84 @@
1
+ # frizen_string_literal: true
2
+
3
+ require 'redis'
4
+ require 'yaml'
5
+
6
+ module AsyncStorage
7
+ class Config
8
+ class << self
9
+ private
10
+
11
+ def attribute_accessor(field, validator: nil, normalizer: nil, default: nil)
12
+ normalizer ||= :"normalize_#{field}"
13
+ validator ||= :"validate_#{field}"
14
+
15
+ define_method(field) do
16
+ unless instance_variable_defined?(:"@#{field}")
17
+ fallback = config_from_yaml[field.to_s] || default
18
+ return if fallback.nil?
19
+
20
+ send(:"#{field}=", fallback.respond_to?(:call) ? fallback.call : fallback)
21
+ end
22
+ instance_variable_get(:"@#{field}")
23
+ end
24
+
25
+ define_method(:"#{field}=") do |value|
26
+ value = send(normalizer, field, value) if respond_to?(normalizer, true)
27
+ send(validator, field, value) if respond_to?(validator, true)
28
+
29
+ instance_variable_set(:"@#{field}", value)
30
+ end
31
+ end
32
+ end
33
+
34
+ # Path to the YAML file with configs
35
+ attr_accessor :config_path
36
+
37
+ # Redis/ConnectionPool instance of a valid list of configs to build a new redis connection
38
+ attribute_accessor :redis, default: nil
39
+
40
+ # Namespace used to group group data stored by this package
41
+ attribute_accessor :namespace, default: 'async_storage'
42
+
43
+ # The global TTL for the redis storage. Keep nil if you don't want to expire objects.
44
+ attribute_accessor :expires_in, default: nil
45
+
46
+ def config_path=(value)
47
+ @config_from_yaml = nil
48
+ @config_path = value
49
+ end
50
+
51
+ private
52
+
53
+ def normalize_namespace(_attribute, value)
54
+ return value.to_s if value.is_a?(Symbol)
55
+
56
+ value
57
+ end
58
+
59
+ def validate_namespace(attribute, value)
60
+ return if value.is_a?(String) && !value.empty?
61
+
62
+ raise InvalidConfig, format(
63
+ "The %<value>p for %<attr>s is not valid. It can't be blank",
64
+ value: value,
65
+ attr: attribute,
66
+ )
67
+ end
68
+
69
+ def normalize_expires_in(_attr, value)
70
+ ttl = value.to_i
71
+ return unless ttl > 0
72
+
73
+ ttl
74
+ end
75
+
76
+ def config_from_yaml
77
+ @config_from_yaml ||= begin
78
+ config_path ? YAML.load_file(config_path) : {}
79
+ rescue Errno::ENOENT, Errno::ESRCH
80
+ {}
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+
5
+ module AsyncStorage
6
+ module JSON
7
+ module_function
8
+
9
+ # Parses JSON data.
10
+ #
11
+ # @param data [String] JSON data
12
+ # @param options [Hash] Options hash for `MultiJson.load`
13
+ # @return [Object] Parsed JSON
14
+ # @raise [MultiJson::ParseError] MultiJson error classes
15
+ def load(data, **options)
16
+ MultiJson.load(data, **options)
17
+ end
18
+
19
+ # Generates JSON.
20
+ #
21
+ # @param object [Object] Object to convert to JSON
22
+ # @param options [Hash] Options hash for `MultiJson.dump` and additional options below
23
+ # @return [String] Generated JSON
24
+ # @raise [MultiJson::DecodeError] MultiJson error classes
25
+ def dump(object, **options)
26
+ object = as_json(object)
27
+
28
+ MultiJson.dump(object, **options)
29
+ end
30
+
31
+ def as_json(value)
32
+ case value
33
+ when Hash
34
+ value.transform_values { |val| as_json(val) }
35
+ when Enumerable
36
+ value.map { |val| as_json(val) }
37
+ else
38
+ value
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'async_storage/util/strings'
5
+ require 'async_storage/json'
6
+
7
+ module AsyncStorage
8
+ class Naming
9
+ include Util::Strings
10
+
11
+ SET = {
12
+ head: 'h',
13
+ body: 'b',
14
+ none: '_null_',
15
+ }.freeze
16
+
17
+ attr_reader :class_name, :class_args
18
+ attr_accessor :prefix
19
+
20
+ def initialize(klass, *args)
21
+ @class_name = normalize_class(klass.name)
22
+ @class_args = normalize_args(args)
23
+ end
24
+
25
+ def head
26
+ "#{base}:#{SET[:head]}"
27
+ end
28
+
29
+ def body
30
+ "#{base}:#{SET[:body]}"
31
+ end
32
+
33
+ def to_s
34
+ format(
35
+ '#<AsyncStorage::Naming head=%<head>p body=%<body>p>',
36
+ head: head,
37
+ body: body,
38
+ )
39
+ end
40
+
41
+ def eql?(other)
42
+ return false unless other.is_a?(self.class)
43
+
44
+ [head, body] == [other.head, other.body]
45
+ end
46
+ alias == eql?
47
+
48
+ protected
49
+
50
+ def base
51
+ [ns, prefix, class_name, class_args].compact.join(':')
52
+ end
53
+
54
+ def normalize_class(name)
55
+ if name.nil? || name.empty?
56
+ raise ArgumentError, 'Anonymous class is not allowed'
57
+ end
58
+
59
+ underscore(name, ':')
60
+ end
61
+
62
+ def normalize_args(args)
63
+ return SET[:none] if args.empty?
64
+
65
+ Digest::SHA256.hexdigest(
66
+ AsyncStorage::JSON.dump(args, mode: :compat),
67
+ )
68
+ end
69
+
70
+ def ns
71
+ AsyncStorage.config.namespace
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis'
4
+ require 'forwardable'
5
+
6
+ module AsyncStorage
7
+ class RedisPool
8
+ extend Forwardable
9
+ def_delegator :@connection, :with
10
+
11
+ module ConnectionPoolLike
12
+ def with
13
+ yield self
14
+ end
15
+ end
16
+
17
+ def initialize(connection)
18
+ if connection.respond_to?(:with)
19
+ @connection = connection
20
+ else
21
+ if connection.respond_to?(:client)
22
+ @connection = connection
23
+ else
24
+ @connection = ::Redis.new(*[connection].compact)
25
+ end
26
+ @connection.extend(ConnectionPoolLike)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async_storage/naming'
4
+
5
+ module AsyncStorage
6
+ class Repo
7
+ CTRL = {
8
+ enqueued: '0',
9
+ executed: '1',
10
+ missing: nil,
11
+ }.freeze
12
+
13
+ attr_reader :resolver_class
14
+
15
+ # @param resolver_class [Class] A class with the call method
16
+ # @param options [Hash] A hash with config
17
+ # @option expires_in [Nil, Integer] Time in seconds
18
+ # @raise [ArgumentError] When the resolver_class does not respond with `call' instance method
19
+ def initialize(resolver_class, **options)
20
+ validate_resolver_class!(resolver_class)
21
+ @resolver_class = resolver_class
22
+ @options = options
23
+ end
24
+
25
+ # Store a fresh content into redis. This method is invoked by a background job.
26
+ #
27
+ # @param options [Hash] List of options to be passed along the initializer
28
+ # @option [Class] klass The resolver class
29
+ # @option [Array] args An array with the resolver arguments
30
+ # @return [Hash, NilClass] the result from class resolver
31
+ def self.ack(klass:, args:, **options)
32
+ new(klass, **options).refresh!(*args)
33
+ end
34
+
35
+ # Async get value with a given key
36
+ #
37
+ # @return [Object, NilClass] Return both stale or fresh object. If does not exist async call the retriever and return nil
38
+ def get(*args)
39
+ connection(*args) do |redis, naming|
40
+ raw_head = redis.get(naming.head)
41
+ case raw_head
42
+ when CTRL[:executed], CTRL[:enqueued]
43
+ read(redis, naming.body) # Try to deliver stale content
44
+ when CTRL[:missing]
45
+ return update!(redis, naming, *args) unless async?
46
+
47
+ perform_async(*args) # Enqueue background job to resolve content
48
+ redis.set(naming.head, CTRL[:enqueued])
49
+ read(redis, naming.body) # Try to deliver stale content
50
+ else
51
+ raise AsyncStorage::Error, format('the key %<k>s have an invalid value. Only "1" or "0" values are expected. And we got %<v>p', v: raw_head, k: naming.head)
52
+ end
53
+ end
54
+ end
55
+
56
+ # Sync get value with a given value
57
+ #
58
+ # @return [Object] Return the result from resolver
59
+ def get!(*args)
60
+ connection(*args) do |redis, naming|
61
+ raw_head = redis.get(naming.head)
62
+ case raw_head
63
+ when CTRL[:executed]
64
+ read(redis, naming.body) || begin
65
+ update!(redis, naming, *args) unless redis.exists?(naming.body)
66
+ end
67
+ when CTRL[:missing], CTRL[:enqueued]
68
+ update!(redis, naming, *args)
69
+ else
70
+ raise AsyncStorage::Error, format('the key %<k>s have an invalid value. Only "1" or "0" values are expected. And we got %<v>p', v: raw_head, k: naming.head)
71
+ end
72
+ end
73
+ end
74
+
75
+ # Expire object the object with a given key. The stale object will not be removed
76
+ #
77
+ # @return [Boolean] True or False according to the object existence
78
+ def invalidate(*args)
79
+ connection(*args) do |redis, naming|
80
+ redis.del(naming.head) == 1
81
+ end
82
+ end
83
+
84
+ # Delete object with a given key.
85
+ #
86
+ # @return [Boolean] True or False according to the object existence
87
+ def invalidate!(*args)
88
+ connection(*args) do |redis, naming|
89
+ redis.multi do |cli|
90
+ cli.del(naming.body)
91
+ cli.del(naming.head)
92
+ end.include?(1)
93
+ end
94
+ end
95
+
96
+ # Invalidate object with the given key and update content according to the strategy
97
+ #
98
+ # @return [Object, NilClass] Stale object or nil when it does not exist
99
+ def refresh(*args)
100
+ value = get(*args)
101
+ invalidate(*args)
102
+ value
103
+ end
104
+
105
+ # Fetch data from resolver and store it into redis
106
+ #
107
+ # @return [Object] Return the result from resolver
108
+ def refresh!(*args)
109
+ connection(*args) { |redis, naming| update!(redis, naming, *args) }
110
+ end
111
+
112
+ # Check if a fresh value exist.
113
+ #
114
+ # @return [Boolean] True or False according the object existence
115
+ def exist?(*args)
116
+ connection(*args) { |redis, naming| redis.exists?(naming.head) && redis.exists?(naming.body) }
117
+ end
118
+
119
+ # Check if object with a given key is stale
120
+ #
121
+ # @return [NilClass, Boolean] Return nil if the object does not exist or true/false according to the object freshness state
122
+ def stale?(*args)
123
+ connection(*args) { |redis, naming| redis.exists?(naming.body) && redis.ttl(naming.head) < 0 }
124
+ end
125
+
126
+ # Check if a fresh object exists into the storage
127
+ #
128
+ # @return [Boolean] true/false according to the object existence and freshness
129
+ def fresh?(*args)
130
+ connection(*args) { |redis, naming| redis.exists?(naming.body) && redis.ttl(naming.head) > 0 }
131
+ end
132
+
133
+ private
134
+
135
+ def async?
136
+ false
137
+ end
138
+
139
+ def perform_async(*args)
140
+ # @TODO Enqueue a real background job here. It's only working on sync mode
141
+ # redis.set(name.head, CTRL[:enqueued])
142
+ refresh!(*args)
143
+ end
144
+
145
+ def update!(redis, naming, *args)
146
+ payload = resolver_class.new.(*args)
147
+
148
+ json = AsyncStorage::JSON.dump(payload, mode: :compat)
149
+ naming = build_naming(*args)
150
+ redis.multi do |cli|
151
+ cli.set(naming.body, json)
152
+ cli.set(naming.head, CTRL[:executed])
153
+ cli.expire(naming.head, expires_in) if expires_in
154
+ end
155
+ AsyncStorage::JSON.load(json)
156
+ end
157
+
158
+ def read(redis, key)
159
+ return unless key
160
+
161
+ raw = redis.get(key)
162
+ return unless raw
163
+
164
+ AsyncStorage::JSON.load(raw)
165
+ end
166
+
167
+ def expires_in
168
+ @options[:expires_in] || AsyncStorage.config.expires_in
169
+ end
170
+
171
+ def connection(*args)
172
+ return unless block_given?
173
+
174
+ naming = build_naming(*args)
175
+ AsyncStorage.redis_pool.with { |redis| yield(redis, naming) }
176
+ end
177
+
178
+ def build_naming(*args)
179
+ naming = AsyncStorage::Naming.new(resolver_class, *args)
180
+ naming.prefix = @options[:namespace] if @options[:namespace]
181
+ naming
182
+ end
183
+
184
+ def validate_resolver_class!(klass)
185
+ unless klass.is_a?(Class)
186
+ raise(ArgumentError, format('%<c>p is not a valid resolver class.', c: klass))
187
+ end
188
+
189
+ unless klass.instance_methods.include?(:call)
190
+ raise(ArgumentError, format('%<c>p must have call instance method.', c: klass))
191
+ end
192
+
193
+ true
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsyncStorage
4
+ module Util
5
+ module Strings
6
+ module_function
7
+
8
+ def underscore(string, module_sep = '/')
9
+ string
10
+ .gsub(/::/, module_sep)
11
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
12
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
13
+ .tr('-', '_')
14
+ .downcase
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsyncStorage
4
+ VERSION = '0.0.1'
5
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async_storage
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Marcos G. Zimmermann
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-11-09 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.0.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.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: multi_json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.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.0.0
41
+ description: Asynchronous key-value storage system
42
+ email:
43
+ - mgzmaster@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - ".rspec"
50
+ - ".travis.yml"
51
+ - Gemfile
52
+ - Gemfile.lock
53
+ - LICENSE.txt
54
+ - README.md
55
+ - Rakefile
56
+ - async_storage.gemspec
57
+ - bin/console
58
+ - bin/setup
59
+ - lib/async_storage.rb
60
+ - lib/async_storage/bath_actions.rb
61
+ - lib/async_storage/config.rb
62
+ - lib/async_storage/json.rb
63
+ - lib/async_storage/naming.rb
64
+ - lib/async_storage/redis_pool.rb
65
+ - lib/async_storage/repo.rb
66
+ - lib/async_storage/util/strings.rb
67
+ - lib/async_storage/version.rb
68
+ homepage: https://github.com/marcosgz/async_storage
69
+ licenses:
70
+ - MIT
71
+ metadata:
72
+ homepage_uri: https://github.com/marcosgz/async_storage
73
+ source_code_uri: https://github.com/marcosgz/async_storage
74
+ changelog_uri: https://github.com/marcosgz/async_storage/blob/master/CHANGELOG.md
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 2.3.0
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 3.1.2
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: Asynchronous key-value storage system
94
+ test_files: []