cache_box 0.0.1.pre.preview4 → 0.0.1.pre.preview9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/cache_box.gemspec +21 -5
- data/lib/cache_box.rb +41 -162
- 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 +197 -0
- metadata +38 -12
- 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
|