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.
@@ -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