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