conflow 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/.codeclimate.yml +6 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +17 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +69 -0
- data/LICENSE.txt +21 -0
- data/README.md +146 -0
- data/Rakefile +8 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/conflow.gemspec +32 -0
- data/lib/conflow.rb +49 -0
- data/lib/conflow/flow.rb +60 -0
- data/lib/conflow/flow/job_handler.rb +70 -0
- data/lib/conflow/job.rb +37 -0
- data/lib/conflow/redis/add_job_script.rb +36 -0
- data/lib/conflow/redis/array_field.rb +81 -0
- data/lib/conflow/redis/complete_job_script.rb +32 -0
- data/lib/conflow/redis/connection_wrapper.rb +19 -0
- data/lib/conflow/redis/field.rb +46 -0
- data/lib/conflow/redis/field_builder.rb +44 -0
- data/lib/conflow/redis/findable.rb +28 -0
- data/lib/conflow/redis/hash_field.rb +74 -0
- data/lib/conflow/redis/identifier.rb +66 -0
- data/lib/conflow/redis/model.rb +55 -0
- data/lib/conflow/redis/script.rb +70 -0
- data/lib/conflow/redis/sorted_set_field.rb +141 -0
- data/lib/conflow/redis/value_field.rb +50 -0
- data/lib/conflow/version.rb +6 -0
- data/lib/conflow/worker.rb +32 -0
- metadata +190 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conflow
|
4
|
+
module Redis
|
5
|
+
# Findable module allows to use .find method on models with identifiers. It requires additional field: :type
|
6
|
+
module Findable
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
base.field :type, :value
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
super
|
14
|
+
self.type = self.class.name
|
15
|
+
end
|
16
|
+
|
17
|
+
# Adds .find method which accepts ID and returns model of proper (sub)type
|
18
|
+
module ClassMethods
|
19
|
+
def find(id)
|
20
|
+
class_name = ValueField.new(format(key_template + ":type", id: id)).value
|
21
|
+
raise ::Redis::CommandError, "#{name} with ID #{id} doesn't exist" unless class_name
|
22
|
+
|
23
|
+
Object.const_get(class_name).new(id)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conflow
|
4
|
+
module Redis
|
5
|
+
# Represents Redis hash. It's methods mirror most used Hash methods.
|
6
|
+
class HashField < Field
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def [](field)
|
10
|
+
value = command(:hget, [key, field])
|
11
|
+
value ? JSON.parse(value) : value
|
12
|
+
end
|
13
|
+
|
14
|
+
def []=(field, value)
|
15
|
+
command :hset, [key, field, JSON.dump(value)]
|
16
|
+
end
|
17
|
+
|
18
|
+
def merge(hash)
|
19
|
+
command :hmset, [key, hash.flatten]
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete(*fields)
|
23
|
+
command :hdel, [key, fields]
|
24
|
+
end
|
25
|
+
|
26
|
+
def overwrite(new_hash)
|
27
|
+
redis.with do |conn|
|
28
|
+
conn.pipelined do
|
29
|
+
conn.del(key)
|
30
|
+
conn.hmset(key, prepare_hash(new_hash).flatten)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def keys
|
36
|
+
command(:hkeys, [key]).map(&:to_sym)
|
37
|
+
end
|
38
|
+
|
39
|
+
def size
|
40
|
+
command :hlen, [key]
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_h
|
44
|
+
command(:hgetall, [key]).each_with_object({}) do |(key, value), hash|
|
45
|
+
hash[key.to_sym] = JSON.parse(value)
|
46
|
+
end
|
47
|
+
end; alias to_hash to_h
|
48
|
+
|
49
|
+
def each(&block)
|
50
|
+
to_h.each(&block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def ==(other)
|
54
|
+
case other
|
55
|
+
when Hash then to_h == other
|
56
|
+
when HashField then key == other.key || to_h == other.to_h
|
57
|
+
else super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
to_h.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def prepare_hash(hash)
|
68
|
+
hash.each_with_object({}) do |(k, v), h|
|
69
|
+
h[k] = v && JSON.dump(v)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conflow
|
4
|
+
module Redis
|
5
|
+
# Identifier changes logic of fields so that they can be found by an id.
|
6
|
+
# ID is a counter stored in redis under .counter_key
|
7
|
+
# Key is build with template stored in .key_template
|
8
|
+
module Identifier
|
9
|
+
# Extends base class with {ClassMethods}
|
10
|
+
def self.included(base)
|
11
|
+
base.extend ClassMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
# class methods for classes with identifier
|
15
|
+
module ClassMethods
|
16
|
+
attr_writer :counter_key, :key_template
|
17
|
+
|
18
|
+
# Copies *counter_key* and *key_template* to child classes
|
19
|
+
def inherited(base)
|
20
|
+
base.instance_variable_set("@counter_key", counter_key)
|
21
|
+
base.instance_variable_set("@key_template", key_template)
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
# Redis key holding counter with IDs of model.
|
26
|
+
# @example default key
|
27
|
+
# class My::Super::Class < Conflow::Redis::Field
|
28
|
+
# include Conflow::Redis::Identifier
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# My::Super::Class.counter_key #=> "my:super:class:idcnt"
|
32
|
+
# @return [String] Redis key
|
33
|
+
def counter_key
|
34
|
+
@counter_key ||= [*name.downcase.split("::"), :idcnt].join(":")
|
35
|
+
end
|
36
|
+
|
37
|
+
# Template for building keys for fields using only the ID.
|
38
|
+
# @example default key
|
39
|
+
# class My::Super::Class < Conflow::Redis::Field
|
40
|
+
# include Conflow::Redis::Identifier
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# My::Super::Class.key_template #=> "my:super:class:%<id>d"
|
44
|
+
# @return [String] Template for building redis keys
|
45
|
+
def key_template
|
46
|
+
@key_template ||= [*name.downcase.split("::"), "%<id>d"].join(":")
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Integer] next available ID
|
50
|
+
def generate_id
|
51
|
+
Conflow.redis.with { |conn| conn.incr(counter_key) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# ID of the model
|
56
|
+
attr_reader :id
|
57
|
+
|
58
|
+
# Overrides logic of {Field#initialize}, allowing creating objects with ID instead of full key
|
59
|
+
# @param id [Integer] ID of the model
|
60
|
+
def initialize(id = self.class.generate_id)
|
61
|
+
@id = id.to_i
|
62
|
+
super(format(self.class.key_template, id: id))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conflow
|
4
|
+
module Redis
|
5
|
+
# Models adds .field method which allows to define fields easily.
|
6
|
+
module Model
|
7
|
+
# Extends base class with .field and .has_many methods
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
# - if other object is a {Model} as well, it compares keys
|
13
|
+
# - standard Ruby comparison otherwise
|
14
|
+
# @return [Boolean] true if other is the same model, false otherwise
|
15
|
+
def ==(other)
|
16
|
+
other.is_a?(Model) ? key == other.key : super
|
17
|
+
end
|
18
|
+
|
19
|
+
# Methods for defining fields on model
|
20
|
+
module ClassMethods
|
21
|
+
# Defines Redis field accessors.
|
22
|
+
# @param name [Symbol] name of the field
|
23
|
+
# @param type [:hash, :array, :value, :sorted_set] type of the new field
|
24
|
+
#
|
25
|
+
# @see Conflow::Redis::HashField
|
26
|
+
# @see Conflow::Redis::ArrayField
|
27
|
+
# @see Conflow::Redis::ValueField
|
28
|
+
# @see Conflow::Redis::SortedSetField
|
29
|
+
# @example
|
30
|
+
# model_class.field :data, :hash
|
31
|
+
# instance = model_class.new
|
32
|
+
# instance.hash["something"] = 800
|
33
|
+
# instance.hash = { something: "else"}
|
34
|
+
# instance.hash["something"] #=> "else"
|
35
|
+
def field(name, type)
|
36
|
+
case type
|
37
|
+
when :hash then FieldBuilder.new(name, Conflow::Redis::HashField).call(self)
|
38
|
+
when :array then FieldBuilder.new(name, Conflow::Redis::ArrayField).call(self)
|
39
|
+
when :value then FieldBuilder.new(name, Conflow::Redis::ValueField).call(self)
|
40
|
+
when :sorted_set then FieldBuilder.new(name, Conflow::Redis::SortedSetField).call(self)
|
41
|
+
else raise ArgumentError, "Unknown type: #{type}. Should be one of: [:hash, :array]"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Convienience method for defining relation-like accessor.
|
46
|
+
# @example
|
47
|
+
# has_many :jobs, Conflow::Job # defines #job_ids and #jobs
|
48
|
+
def has_many(name, klass, field_name: "#{name.to_s.chop}_ids")
|
49
|
+
field(field_name, :array)
|
50
|
+
define_method(name) { send(field_name).map { |id| klass.new(id) } }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conflow
|
4
|
+
module Redis
|
5
|
+
# Main class for scripts, handling logic of executing and caching scripts.
|
6
|
+
class Script
|
7
|
+
class << self
|
8
|
+
# @return [Boolean] whether scripts are cached or not
|
9
|
+
attr_reader :cache_scripts
|
10
|
+
# @return [String] LUA script of this Conflow::Redis::Script
|
11
|
+
attr_reader :script
|
12
|
+
|
13
|
+
# Sets cache_scripts option on inherited scripts
|
14
|
+
def inherited(base)
|
15
|
+
scripts << base
|
16
|
+
base.cache_scripts = cache_scripts
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
# @!attribute [rw] cache_scripts
|
21
|
+
# This options decides whether scripts used by the gem will be cached in Redis or not.
|
22
|
+
# See {https://redis.io/commands/eval Redis EVAL} and {https://redis.io/commands/evalsha Redis EVALSHA}.
|
23
|
+
# @example Disable caching scripts (set this in your initializer)
|
24
|
+
# Conflow::Redis::Script.cache_scripts = false
|
25
|
+
def cache_scripts=(value)
|
26
|
+
@cache_scripts = value
|
27
|
+
|
28
|
+
@command = value ? :sha_eval : :eval
|
29
|
+
scripts.each { |script_class| script_class.cache_scripts = cache_scripts }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Executes script in Redis with given arguments.
|
33
|
+
#
|
34
|
+
# @overload call(keys, args = [])
|
35
|
+
# @param keys [Array<String>] Array of keys
|
36
|
+
# @param args [Array<Object>] Array of arguments of the script
|
37
|
+
def call(*args)
|
38
|
+
Conflow.redis.with { |conn| send(command, conn, args) }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :command, :sha
|
44
|
+
|
45
|
+
def script=(script)
|
46
|
+
@script = script
|
47
|
+
@sha = Digest::SHA1.hexdigest(script)
|
48
|
+
end
|
49
|
+
|
50
|
+
def scripts
|
51
|
+
@scripts ||= []
|
52
|
+
end
|
53
|
+
|
54
|
+
def sha_eval(redis, args)
|
55
|
+
redis.evalsha(sha, *args)
|
56
|
+
rescue ::Redis::CommandError => e
|
57
|
+
raise unless e.message == "NOSCRIPT No matching script. Please use EVAL."
|
58
|
+
redis.script(:load, script)
|
59
|
+
retry
|
60
|
+
end
|
61
|
+
|
62
|
+
def eval(redis, args)
|
63
|
+
redis.eval(script, *args)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
self.cache_scripts = true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conflow
|
4
|
+
module Redis
|
5
|
+
# Represents Redis sorted set. Closest Ruby representation is a Hash
|
6
|
+
# where keys are elements of the set and values represent score.
|
7
|
+
class SortedSetField < Field
|
8
|
+
# Adds one or more keys to the set.
|
9
|
+
# @param hash [Hash] hash of values and scores to be added
|
10
|
+
# @return [String] Redis response
|
11
|
+
#
|
12
|
+
# @example Adding multiple fields
|
13
|
+
# field.add(last: 10, tied: 2, second: 4, first: 2)
|
14
|
+
def add(hash)
|
15
|
+
command :zadd, [key, hash_to_array(hash)]
|
16
|
+
end
|
17
|
+
|
18
|
+
# Access score of given element.
|
19
|
+
# @param value [String, Symbol] element of the set
|
20
|
+
# @return [String] Score of the element (nil if element not present in set)
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# field[:last] #=> 10
|
24
|
+
def [](value)
|
25
|
+
command :zscore, [key, value]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Set score of given element.
|
29
|
+
# @param value [String, Symbol] element of the set
|
30
|
+
# @param rank [Numeric] score to be assigned
|
31
|
+
# @return [Integer] Number of added elements (1 if key didn't exist, 0 otherwise)
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# field[:last] = 24 #=> 0
|
35
|
+
def []=(value, rank)
|
36
|
+
command :zadd, [key, rank, value]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Number of elements in the set
|
40
|
+
# @return [Integer] Size of the set
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# field.size #=> 4
|
44
|
+
def size
|
45
|
+
command :zcard, [key]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Remove element from the set.
|
49
|
+
# @param value [String, Symbol] element of the set
|
50
|
+
# @return [Integer] Number of removed elements (1 if key existed, 0 otherwise)
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# field.delete(:last) #=> 1
|
54
|
+
def delete(value)
|
55
|
+
command :zrem, [key, value]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return elements with given score
|
59
|
+
# @param score [Numeric, Hash]
|
60
|
+
# - when Numeric, only elements with that exact score will be returned
|
61
|
+
# - when Hash, elements within min..max range will be returned. See {https://redis.io/commands/zrange Redis docs}
|
62
|
+
# @option score [String, Numeric] :min minimal score
|
63
|
+
# @option score [String, Numeric] :max maximal score
|
64
|
+
# @return [Array<String>] Elements with given score
|
65
|
+
#
|
66
|
+
# @example with specific score
|
67
|
+
# field.where(score: 2) #=> ["first", "tie"]
|
68
|
+
# @example with only min set
|
69
|
+
# field.where(score: { min: 3 }) #=> ["last", "second"]
|
70
|
+
# @example with both min and max set
|
71
|
+
# field.where(score: { min: 3, max: "(10" }) #=> ["last"]
|
72
|
+
def where(score:)
|
73
|
+
command :zrangebyscore, [key, *prepare_score_bounds(score)]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Removes elements of the set with given score and returns them.
|
77
|
+
# See {where} for details on how to choose score.
|
78
|
+
# @return [Array<String>] Elements with given score
|
79
|
+
def delete_if(score:)
|
80
|
+
score_bounds = prepare_score_bounds(score)
|
81
|
+
|
82
|
+
transaction do |conn|
|
83
|
+
conn.zrangebyscore key, *score_bounds
|
84
|
+
conn.zremrangebyscore key, *score_bounds
|
85
|
+
end[0]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns first *n* elements of the sorted set
|
89
|
+
# @param num [Integer] amount of elements to be returned. Defaults to 1.
|
90
|
+
# @return [String, Array<String>] first *num* elements from the set
|
91
|
+
def first(num = 1)
|
92
|
+
result = command :zrange, [key, 0, num - 1]
|
93
|
+
num == 1 ? result[0] : result
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns last *n* elements of the sorted set
|
97
|
+
# @param num [Integer] amount of elements to be returned. Defaults to 1.
|
98
|
+
# @return [String, Array<String>] last *num* elements from the set
|
99
|
+
def last(num = 1)
|
100
|
+
result = command :zrevrange, [key, 0, num - 1]
|
101
|
+
num == 1 ? result[0] : result
|
102
|
+
end
|
103
|
+
|
104
|
+
# Creates regular Ruby Hash based on Redis values.
|
105
|
+
# @return [Hash] Hash representing this Sorted set
|
106
|
+
def to_h
|
107
|
+
Hash[command :zrange, [key, 0, -1, with_scores: true]]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Removes old values from the set and overrides them with new.
|
111
|
+
# @param hash [Hash] new values of the set
|
112
|
+
# @return [String] Redis response
|
113
|
+
def overwrite(hash)
|
114
|
+
redis.with do |conn|
|
115
|
+
conn.pipelined do
|
116
|
+
conn.del(key)
|
117
|
+
conn.zadd(key, hash_to_array(hash))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def hash_to_array(hash)
|
125
|
+
ary = Array.new(hash.size * 2)
|
126
|
+
|
127
|
+
hash.each_with_object(ary).with_index do |((value, score), result), index|
|
128
|
+
result[index * 2] = score
|
129
|
+
result[index * 2 + 1] = value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def prepare_score_bounds(score)
|
134
|
+
case score
|
135
|
+
when Hash then { min: "-inf", max: "+inf" }.merge(score).values
|
136
|
+
when Numeric then [score, score]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Conflow
|
4
|
+
module Redis
|
5
|
+
# Represents single value (Redis String). Values are serialized as JSON in order to preserve type.
|
6
|
+
class ValueField < Field
|
7
|
+
# @note *value* must be serializable through JSON.dump
|
8
|
+
# @param value [Object] new value to be saved
|
9
|
+
# @return [String] Redis response
|
10
|
+
def overwrite(value)
|
11
|
+
command :set, [key, JSON.dump(value)]
|
12
|
+
end
|
13
|
+
|
14
|
+
# @note *value* must be serializable through JSON.dump
|
15
|
+
# @param value [Object] value to be assigned to field (unless key already holds value)
|
16
|
+
# @return [String] Redis response
|
17
|
+
def default(value)
|
18
|
+
command :set, [key, JSON.dump(value), nx: true]
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param other [Object] Object to compare value with. Handles Strings, Numerics,
|
22
|
+
# Symbols and other {ValueField} objects
|
23
|
+
# @return [String] Redis response
|
24
|
+
def ==(other)
|
25
|
+
case other
|
26
|
+
when String, Numeric then value == other
|
27
|
+
when Symbol then value.to_sym == other
|
28
|
+
when ValueField then key == other.key || to_s == other.to_s
|
29
|
+
else super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Boolean] true if object does not exist in Redis, else otherwise
|
34
|
+
def nil?
|
35
|
+
value.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [String, nil] String representation of value
|
39
|
+
def to_s
|
40
|
+
value&.to_s
|
41
|
+
end; alias to_str to_s
|
42
|
+
|
43
|
+
# @return [Object] JSON-parsed value present in Redis
|
44
|
+
def value
|
45
|
+
result = command(:get, [key])
|
46
|
+
result && JSON.parse(result)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|