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