cache_box 0.0.1.pre.preview5 → 0.0.1.pre.preview10
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 +4 -4
- data/cache_box.gemspec +21 -5
- data/lib/cache_box.rb +41 -161
- data/lib/cache_box/box.rb +17 -0
- data/lib/cache_box/chain.rb +90 -0
- data/lib/cache_box/graph.rb +87 -0
- data/lib/cache_box/helper/validate.rb +139 -0
- data/lib/cache_box/scheduler/base.rb +109 -0
- data/lib/cache_box/scheduler/concurrent.rb +91 -0
- data/lib/cache_box/scheduler/serial.rb +66 -0
- data/lib/cache_box/stash.rb +43 -0
- data/lib/cache_box/storage/file.rb +130 -0
- data/lib/cache_box/storage/memory.rb +207 -0
- data/lib/cache_box/unit.rb +199 -0
- metadata +34 -8
- data/lib/cache_box_chain.rb +0 -177
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CacheBox
|
4
|
+
module Helper
|
5
|
+
# Common validation methods.
|
6
|
+
module Validate
|
7
|
+
# Input:
|
8
|
+
#
|
9
|
+
# arg = String | Symbol
|
10
|
+
# name = String
|
11
|
+
#
|
12
|
+
# Output: arg
|
13
|
+
def validate_string_or_symbol!(arg, name)
|
14
|
+
return arg if arg.is_a?(Symbol) || arg.is_a?(String)
|
15
|
+
|
16
|
+
klass = arg.class
|
17
|
+
value = arg.inspect
|
18
|
+
|
19
|
+
raise(ArgumentError, "#{name} must be a Symbol or String, got #{klass}: #{value}")
|
20
|
+
end
|
21
|
+
|
22
|
+
# Input:
|
23
|
+
#
|
24
|
+
# args = Array[String | Symbol]
|
25
|
+
# name = String
|
26
|
+
#
|
27
|
+
# Output: args
|
28
|
+
def validate_array_of_string_or_symbol!(args, name)
|
29
|
+
unless args.is_a?(Array)
|
30
|
+
klass = args.class
|
31
|
+
value = args.inspect
|
32
|
+
|
33
|
+
raise(
|
34
|
+
ArgumentError,
|
35
|
+
"#{name} must be an Array, got #{klass}: #{value}"
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
args.each do |arg|
|
40
|
+
next if arg.is_a?(Symbol) || arg.is_a?(String)
|
41
|
+
|
42
|
+
klass = arg.class
|
43
|
+
value = arg.inspect
|
44
|
+
|
45
|
+
raise(
|
46
|
+
ArgumentError,
|
47
|
+
"#{name} must contain Symbol or String, got #{klass}: #{value} in #{args}"
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
args
|
52
|
+
end
|
53
|
+
|
54
|
+
# Input:
|
55
|
+
#
|
56
|
+
# block = &block
|
57
|
+
# name = String
|
58
|
+
#
|
59
|
+
# Output: N/A
|
60
|
+
def validate_block_presence!(block, name)
|
61
|
+
return block if block
|
62
|
+
|
63
|
+
raise(ArgumentError, "The `#{name}` method requires a block")
|
64
|
+
end
|
65
|
+
|
66
|
+
# Input:
|
67
|
+
#
|
68
|
+
# edge = String |
|
69
|
+
# Symbol |
|
70
|
+
# Hash{1 String | Symbol => String | Symbol | Array[...String | Symbol]}
|
71
|
+
#
|
72
|
+
# Output: edge
|
73
|
+
def validate_edge!(edge)
|
74
|
+
return edge if edge.is_a?(Symbol) || edge.is_a?(String)
|
75
|
+
|
76
|
+
if edge.is_a?(Hash)
|
77
|
+
validate_hash_edge!(edge)
|
78
|
+
|
79
|
+
edge
|
80
|
+
else
|
81
|
+
klass = edge.class
|
82
|
+
value = edge.inspect
|
83
|
+
|
84
|
+
raise(
|
85
|
+
ArgumentError,
|
86
|
+
"edge has an unknown structure; #{klass}: #{value}"
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Input:
|
94
|
+
#
|
95
|
+
# edge = Hash{1 String | Symbol => String | Symbol | Array[...String | Symbol]}
|
96
|
+
#
|
97
|
+
# Output: edge
|
98
|
+
def validate_hash_edge!(edge)
|
99
|
+
if (size = edge.size) != 1
|
100
|
+
raise(
|
101
|
+
ArgumentError,
|
102
|
+
"edge must have one mapping, got #{size}: #{edge.inspect}"
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
name = edge.keys.first
|
107
|
+
needs = edge[name]
|
108
|
+
|
109
|
+
validate_string_or_symbol!(name, 'edge name')
|
110
|
+
validate_needs!(needs)
|
111
|
+
|
112
|
+
edge
|
113
|
+
end
|
114
|
+
|
115
|
+
# Input:
|
116
|
+
#
|
117
|
+
# arg = String | Symbol | Array[...String | Symbol]
|
118
|
+
#
|
119
|
+
# Output: arg
|
120
|
+
def validate_needs!(arg)
|
121
|
+
return arg if arg.is_a?(Symbol) || arg.is_a?(String)
|
122
|
+
|
123
|
+
if arg.is_a?(Array)
|
124
|
+
validate_array_of_string_or_symbol!(arg, 'edge needs')
|
125
|
+
|
126
|
+
arg
|
127
|
+
else
|
128
|
+
klass = arg.class
|
129
|
+
value = arg.inspect
|
130
|
+
|
131
|
+
raise(
|
132
|
+
ArgumentError,
|
133
|
+
"edge needs has an unknown structure; #{klass}: #{value}"
|
134
|
+
)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CacheBox
|
4
|
+
module Scheduler
|
5
|
+
class Base
|
6
|
+
include CacheBox::Helper::Validate
|
7
|
+
|
8
|
+
# Input:
|
9
|
+
#
|
10
|
+
# logger = Logger
|
11
|
+
#
|
12
|
+
# Output: N/A
|
13
|
+
def initialize(logger: nil)
|
14
|
+
@logger = logger || Logger.new(STDOUT, level: Logger::INFO)
|
15
|
+
|
16
|
+
@lookup = {}
|
17
|
+
@needs = {}
|
18
|
+
@feeds = {}
|
19
|
+
@roots = []
|
20
|
+
@leaves = []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Input:
|
24
|
+
#
|
25
|
+
# edge = String |
|
26
|
+
# Symbol |
|
27
|
+
# Hash{String | Symbol => String | Symbol | Array[String | Symbol]}
|
28
|
+
#
|
29
|
+
# Output: self
|
30
|
+
def add(edge)
|
31
|
+
validate_edge!(edge)
|
32
|
+
|
33
|
+
needs_o = extract_needs(edge)
|
34
|
+
needs_s = needs_o.map(&:to_s)
|
35
|
+
needs_s.each_with_index do |arg, index|
|
36
|
+
next if @lookup.key?(arg)
|
37
|
+
|
38
|
+
raise(
|
39
|
+
ArgumentError,
|
40
|
+
"Graph does not contain required node #{needs_o[index].inspect}"
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
name_o = extract_name(edge)
|
45
|
+
name_s = name_o.to_s
|
46
|
+
if @lookup.key?(name_s)
|
47
|
+
raise(
|
48
|
+
ArgumentError,
|
49
|
+
"Graph already contains a node named #{name_o.inspect}"
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
@lookup[name_s] = name_o
|
54
|
+
@needs[name_s] = needs_s
|
55
|
+
|
56
|
+
@leaves << name_s
|
57
|
+
needs_s.each do |need_s|
|
58
|
+
existing = @feeds[need_s]
|
59
|
+
|
60
|
+
@feeds[need_s] = existing ? existing << name_s : [name_s]
|
61
|
+
@leaves.delete(need_s)
|
62
|
+
end
|
63
|
+
@roots << name_s if needs_s == []
|
64
|
+
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# Input:
|
69
|
+
#
|
70
|
+
# nodes = Hash{...String => Array[String | Symbol, Proc(Object)]}
|
71
|
+
# unit = CacheBox::Unit
|
72
|
+
#
|
73
|
+
# Output: Hash{...String | Symbol => Object}
|
74
|
+
def run!(_nodes, _unit)
|
75
|
+
raise(
|
76
|
+
NotImplementedError,
|
77
|
+
'The `#run/2` method needs to be implemented in your subclass!'
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Input: String | Symbol | Hash[1 String | Symbol => Object]
|
84
|
+
#
|
85
|
+
# Output: String | Symbol
|
86
|
+
def extract_name(edge)
|
87
|
+
return edge.dup if edge.is_a?(Symbol) || edge.is_a?(String)
|
88
|
+
|
89
|
+
edge.keys.first.dup
|
90
|
+
end
|
91
|
+
|
92
|
+
# Input: ?
|
93
|
+
#
|
94
|
+
# Output: Array[...String | Symbol]
|
95
|
+
def extract_needs(edge)
|
96
|
+
return [] unless edge.is_a?(Hash)
|
97
|
+
|
98
|
+
name = edge.keys.first
|
99
|
+
needs = edge[name]
|
100
|
+
|
101
|
+
if needs.is_a?(Array)
|
102
|
+
needs.map(&:dup)
|
103
|
+
else
|
104
|
+
[needs.dup]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
class CacheBox
|
6
|
+
module Scheduler
|
7
|
+
class Concurrent < Base
|
8
|
+
|
9
|
+
# TODO: write and use a thread pool
|
10
|
+
|
11
|
+
# Input:
|
12
|
+
#
|
13
|
+
# nodes = Hash{...String => Array[String | Symbol, Proc(Object)]}
|
14
|
+
# unit = CacheBox::Unit
|
15
|
+
#
|
16
|
+
# Output: Hash{...String | Symbol => Object}
|
17
|
+
def run!(nodes, unit)
|
18
|
+
work = @roots.dup
|
19
|
+
threads = []
|
20
|
+
|
21
|
+
condition = ConditionVariable.new
|
22
|
+
|
23
|
+
condition_lock = Mutex.new
|
24
|
+
scheduler_lock = Mutex.new
|
25
|
+
|
26
|
+
scheduler = Thread.new do
|
27
|
+
condition_lock.synchronize do
|
28
|
+
index = 0
|
29
|
+
loop do
|
30
|
+
stop = false
|
31
|
+
|
32
|
+
scheduler_lock.synchronize do
|
33
|
+
loop do
|
34
|
+
break if index >= work.size
|
35
|
+
|
36
|
+
name_s = work[index]
|
37
|
+
thread = Thread.new do
|
38
|
+
callable = nodes[name_s][1]
|
39
|
+
needs = @needs[name_s]
|
40
|
+
input = {}
|
41
|
+
|
42
|
+
scheduler_lock.synchronize do
|
43
|
+
needs.each do |need|
|
44
|
+
next unless unit.has?(need)
|
45
|
+
|
46
|
+
input[@lookup[need]] = unit.result(need)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
callable.call(input)
|
51
|
+
|
52
|
+
scheduler_lock.synchronize do
|
53
|
+
@feeds[name_s]&.each do |feed|
|
54
|
+
work.push(feed) unless work.include?(feed)
|
55
|
+
end
|
56
|
+
|
57
|
+
threads.delete(Thread.current)
|
58
|
+
end
|
59
|
+
|
60
|
+
condition_lock.synchronize do
|
61
|
+
condition.signal
|
62
|
+
end
|
63
|
+
end
|
64
|
+
threads << thread
|
65
|
+
|
66
|
+
index += 1
|
67
|
+
end
|
68
|
+
|
69
|
+
stop = true if threads.empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
break if stop
|
73
|
+
|
74
|
+
condition.wait(condition_lock)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
scheduler.join
|
80
|
+
|
81
|
+
result = {}
|
82
|
+
@leaves.each do |name_s|
|
83
|
+
next unless unit.has?(name_s)
|
84
|
+
|
85
|
+
result[@lookup[name_s]] = unit.result(name_s)
|
86
|
+
end
|
87
|
+
result
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
class CacheBox
|
6
|
+
module Scheduler
|
7
|
+
class Serial < Base
|
8
|
+
# Input:
|
9
|
+
#
|
10
|
+
# nodes = Hash{...String => Array[String | Symbol, Proc(Object)]}
|
11
|
+
# unit = CacheBox::Unit
|
12
|
+
#
|
13
|
+
# Output: Hash{...String | Symbol => Object}
|
14
|
+
def run!(nodes, unit)
|
15
|
+
plan = @leaves.dup.reverse
|
16
|
+
|
17
|
+
plan_index = 0
|
18
|
+
loop do
|
19
|
+
name_s = plan[plan_index]
|
20
|
+
|
21
|
+
unless unit.has?(name_s)
|
22
|
+
needs = @needs[name_s]
|
23
|
+
needs.reverse_each { |need| plan << need }
|
24
|
+
end
|
25
|
+
|
26
|
+
plan_index += 1
|
27
|
+
break if plan_index >= plan.size
|
28
|
+
end
|
29
|
+
|
30
|
+
sequence = plan.reverse
|
31
|
+
sequence.each_with_index do |name_s, index|
|
32
|
+
next if unit.has?(name_s)
|
33
|
+
|
34
|
+
callable = nodes[name_s][1]
|
35
|
+
needs = @needs[name_s]
|
36
|
+
input = {}
|
37
|
+
|
38
|
+
needs.each do |need|
|
39
|
+
next unless unit.has?(need)
|
40
|
+
|
41
|
+
input[@lookup[need]] = unit.result(need)
|
42
|
+
end
|
43
|
+
callable.call(input)
|
44
|
+
|
45
|
+
needs.each do |need|
|
46
|
+
needed = false
|
47
|
+
|
48
|
+
sequence[(index + 1)..-1].each do |item|
|
49
|
+
needed = true if @needs[item].include?(need)
|
50
|
+
end
|
51
|
+
|
52
|
+
unit.clear(need) unless needed
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
result = {}
|
57
|
+
@leaves.each do |name_s|
|
58
|
+
next unless unit.has?(name_s)
|
59
|
+
|
60
|
+
result[@lookup[name_s]] = unit.result(name_s)
|
61
|
+
end
|
62
|
+
result
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CacheBox
|
4
|
+
# A thread safe, in memory, key value storage.
|
5
|
+
#
|
6
|
+
# The object itself is thread safe but it's contents is not.
|
7
|
+
class Stash
|
8
|
+
def initialize(state = nil)
|
9
|
+
@lock = Mutex.new
|
10
|
+
@state = state || {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](key)
|
14
|
+
@lock.synchronize do
|
15
|
+
@state[key]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def []=(key, value)
|
20
|
+
@lock.synchronize do
|
21
|
+
@state[key] = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(key)
|
26
|
+
@lock.synchronize do
|
27
|
+
@state.delete(key)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def key?(key)
|
32
|
+
@lock.synchronize do
|
33
|
+
@state.key?(key)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def with
|
38
|
+
@lock.synchronize do
|
39
|
+
yield(@state)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|