actuator 0.0.1

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,45 @@
1
+ #include <iostream>
2
+ #include <map>
3
+ #include <ruby.h>
4
+
5
+ #include "actuator.h"
6
+ #include "clock.h"
7
+
8
+ class Timer {
9
+ public:
10
+ int id;
11
+ double delay;
12
+ double interval;
13
+ double at;
14
+ VALUE fiber;
15
+ VALUE instance = 0;
16
+ VALUE callback_block;
17
+ std::multimap<double, Timer*>::iterator iterator;
18
+ bool is_scheduled;
19
+ bool is_destroyed;
20
+ char* inspected;
21
+
22
+ Timer();
23
+ Timer(double initial_delay);
24
+ ~Timer();
25
+ void Destroy();
26
+ void SetDelay(double initial_delay);
27
+ void Schedule();
28
+ void Remove();
29
+ void SetCallback(VALUE callback);
30
+ void SetFiber(VALUE current_fiber);
31
+ void SetInitialDelay(VALUE delay);
32
+ void ExpireImmediately();
33
+ void Fire();
34
+
35
+ static void Setup();
36
+ static Timer* Get(VALUE instance);
37
+ static void Clear();
38
+ static void Update(double now);
39
+ static double GetNextEventTime();
40
+ private:
41
+ void InsertIntoSchedule();
42
+ bool RemoveFromSchedule();
43
+ void StartedBeingScheduled();
44
+ void StoppedBeingScheduled();
45
+ };
@@ -0,0 +1,22 @@
1
+ require_relative 'actuator/actuator'
2
+ require_relative 'actuator/job'
3
+ require_relative 'actuator/fiber'
4
+ require_relative 'actuator/fiber_pool'
5
+
6
+ module Actuator
7
+ VERSION = "0.0.1"
8
+
9
+ class << self
10
+ def run
11
+ start { defer { yield } if block_given? }
12
+ end
13
+
14
+ def next_tick
15
+ Timer.in(0) { yield }
16
+ end
17
+
18
+ def defer
19
+ FiberPool.run { yield }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ require 'fiber'
2
+
3
+ class Fiber
4
+ attr_accessor :job, :last_job
5
+ attr_writer :is_root
6
+
7
+ current.is_root = true
8
+ current.job = Job.new
9
+ current.job.id = 0
10
+
11
+ def root?
12
+ @is_root
13
+ end
14
+ end
@@ -0,0 +1,82 @@
1
+ require 'fiber'
2
+
3
+ module Actuator
4
+ class FiberPool
5
+ MAX_FIBERS = 10000
6
+
7
+ @fibers = []
8
+ @idle_fibers = []
9
+ @queued_jobs = []
10
+ @busy_count = 0
11
+
12
+ class << self
13
+ attr_reader :busy_count
14
+
15
+ # Always starts fiber immediately - ignores MAX_FIBERS (can cause pool to grow beyond limit)
16
+ def run(whois=nil, &block)
17
+ job = Job.new
18
+ job.block = block
19
+ job.whois = whois
20
+
21
+ @busy_count += 1
22
+ fiber = @idle_fibers.pop || create_new_fiber
23
+ fiber.resume(job)
24
+
25
+ job
26
+ end
27
+
28
+ # Job will be queued if MAX_FIBERS are already active
29
+ def queue(whois=nil, &block)
30
+ job = Job.new
31
+ job.block = block
32
+ job.whois = whois
33
+
34
+ if @busy_count >= MAX_FIBERS
35
+ @queued_jobs << job
36
+ #puts "[FiberPool] There are already #@busy_count/#{MAX_FIBERS} busy fibers, queued job #{job.id}"
37
+ return false
38
+ end
39
+
40
+ @busy_count += 1
41
+ fiber = @idle_fibers.pop || create_new_fiber
42
+ fiber.resume(job)
43
+
44
+ job
45
+ end
46
+
47
+ def create_new_fiber
48
+ Fiber.new do |job|
49
+ fiber = Fiber.current
50
+ while true
51
+ #if !job || !job.is_a?(Job)
52
+ # Log.error "Job #{fiber.last_job ? fiber.last_job.id : -1} fiber resumed after ending and being released to the pool - #{job.inspect}"
53
+ #end
54
+ job.fiber = fiber
55
+ fiber.job = job
56
+ job.job_started
57
+ begin
58
+ job.block.call
59
+ rescue JobKilledException
60
+ # We use a cached exception to avoid the overhead of a catch block since fibers should almost never be killed
61
+ rescue => ex
62
+ Log.error "#{ex.class} while running job: #{ex.message}\n#{ex.backtrace.join "\n"}"
63
+ raise ex
64
+ rescue SystemExit
65
+ break
66
+ end
67
+ job.job_ended
68
+ #fiber.last_job = job
69
+ if @queued_jobs.empty?
70
+ @busy_count -= 1
71
+ fiber.job = nil
72
+ @idle_fibers << fiber
73
+ job = Fiber.yield
74
+ else
75
+ job = @queued_jobs.shift
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,256 @@
1
+ module Actuator
2
+ class JobKilledException < Exception
3
+ end
4
+
5
+ JobKilled = JobKilledException.new
6
+
7
+ #TODO: Implement Job class in C++ extension so that we can do profiling and extra safety checks with minimal overhead
8
+ class Job
9
+ @@total = 0
10
+ # @@active_jobs = []
11
+
12
+ class << self
13
+ # def active
14
+ # @@active_jobs
15
+ # end
16
+
17
+ def current
18
+ Fiber.current.job
19
+ end
20
+
21
+ def yield
22
+ job = Job.current
23
+ # if job.time_warning_started_at
24
+ # duration = Actuator.now - job.time_warning_started_at
25
+ # job.time_warning_extra ||= 0.0
26
+ # job.time_warning_extra += duration
27
+ # Log.puts "[time_warning] suspended: #{job.time_warning_name} (#{(job.time_warning_extra * 1000).round(3)})" if job.time_warning_extra > 0.0025
28
+ # end
29
+ # if resumed_at = job.resumed_at
30
+ # delta = Actuator.now - resumed_at
31
+ # if delta > 0.0025
32
+ # Log.puts "Warning: Job #{job.id} yielding after #{(delta * 1000).round(3)} ms\nJob execution started from #{job.resumed_caller.join "\n"}"
33
+ # else
34
+ # #Log.puts "Job #{job.id} yielding - #{(delta * 1000).round(3)} ms"
35
+ # end
36
+ # else
37
+ # #Log.puts "Job #{job.id} yielding"
38
+ # end
39
+ job.is_yielded = true
40
+ value = Fiber.yield
41
+ job.is_yielded = false
42
+ if job.ended?
43
+ # job.time_warning_name = nil
44
+ # job.time_warning_started_at = nil
45
+ # job.time_warning_extra = nil
46
+ raise JobKilled
47
+ else
48
+ # now = job.resumed_at = Actuator.now
49
+ # #job.resumed_caller = caller
50
+ # if job.time_warning_started_at
51
+ # Log.puts "[time_warning] resumed: #{job.time_warning_name} (#{(job.time_warning_extra * 1000).round(3)})" if job.time_warning_extra > 0.0025
52
+ # job.time_warning_started_at = now
53
+ # else
54
+ # #Log.puts "Job #{job.id} resumed"
55
+ # end
56
+ end
57
+ value
58
+ end
59
+
60
+ def sleep(seconds)
61
+ job = Job.current
62
+ job.sleep_timer = Timer.in(seconds) do
63
+ job.fiber.resume true
64
+ end
65
+ Job.yield
66
+ ensure
67
+ job.sleep_timer = nil
68
+ end
69
+
70
+ def wait(jobs, timeout=nil)
71
+ job = Job.current
72
+ jobs << job
73
+ if timeout
74
+ job.sleep_timer = Timer.in(timeout) do
75
+ job.sleep_timer = nil
76
+ job.fiber.resume true
77
+ end
78
+ end
79
+ Job.yield
80
+ end
81
+ end
82
+
83
+ attr_accessor :id, :block, :whois, :thread_locals, :system_thread_locals, :fiber, :sleep_timer, :mutex_asleep, :joined_on, :is_yielded, :resumed_at, :resumed_caller, :time_warning_started_at, :time_warning_extra, :time_warning_name
84
+ attr_writer :thread_variables
85
+
86
+ def job_started
87
+ raise "[Job #@id] job_started called multiple times" if @id
88
+ @id = @@total += 1
89
+ # @@active_jobs << self
90
+ end
91
+
92
+ def job_ended
93
+ @has_ended = true
94
+ @resumed_at = nil
95
+ # @resumed_caller = nil
96
+ # @@active_jobs.delete self
97
+ if defined? @joined_jobs
98
+ joined_jobs = @joined_jobs
99
+ @joined_jobs = nil
100
+ joined_jobs.each(&:resume)
101
+ end
102
+ end
103
+
104
+ def yielded?
105
+ @is_yielded
106
+ end
107
+
108
+ def asleep?
109
+ @sleep_timer || @mutex_asleep
110
+ end
111
+
112
+ def alive?
113
+ !@has_ended
114
+ end
115
+
116
+ def ended?
117
+ @has_ended
118
+ end
119
+
120
+ def sleep(seconds)
121
+ @sleep_timer = Timer.in(seconds) do
122
+ @sleep_timer = nil
123
+ @fiber.resume true
124
+ end
125
+ Job.yield
126
+ end
127
+
128
+ def schedule
129
+ return if @is_scheduled
130
+ @is_scheduled = true
131
+ @sleep_timer.destroy if @sleep_timer
132
+ @sleep_timer = Timer.in(0) do
133
+ @is_scheduled = false
134
+ @sleep_timer = nil
135
+ @fiber.resume
136
+ end
137
+ end
138
+
139
+ def wake!
140
+ unless timer = @sleep_timer
141
+ raise "Tried to wake up a job which is not asleep"
142
+ end
143
+ @sleep_timer = nil
144
+ timer.fire!
145
+ end
146
+
147
+ def join
148
+ return if @has_ended
149
+ fiber = Fiber.current
150
+ job = fiber.job
151
+ begin
152
+ job.joined_on = self
153
+ (@joined_jobs ||= []) << fiber
154
+ Job.yield
155
+ raise "Job#join - resumed before job #@id ended" unless @has_ended
156
+ ensure
157
+ job.joined_on = nil
158
+ @joined_jobs.delete fiber if @joined_jobs
159
+ end
160
+ end
161
+
162
+ def kill
163
+ return if @has_ended
164
+ raise JobKilled if Job.current == self
165
+ if @sleep_timer
166
+ # puts "Kill requested by job #{Job.current.id} (asleep)"
167
+ @sleep_timer.destroy
168
+ @sleep_timer = nil
169
+ elsif @mutex_asleep
170
+ # puts "Kill requested by job #{Job.current.id} (mutex)"
171
+ @mutex_asleep = nil
172
+ elsif @is_yielded
173
+ # puts "Kill requested by job #{Job.current.id} (yielded)"
174
+ else
175
+ raise "[Job #{Job.current.id}] Fiber#kill called on job #{@id} which is #{state}"
176
+ end
177
+ @has_ended = true
178
+ @fiber.resume
179
+ end
180
+
181
+ def thread_variable_get(name)
182
+ @thread_variables[name] if @thread_variables
183
+ end
184
+
185
+ def thread_variable_set(name, value)
186
+ if @thread_variables
187
+ @thread_variables[name] = value
188
+ else
189
+ @thread_variables = { name => value }
190
+ end
191
+ end
192
+
193
+ def state
194
+ if @has_ended; 'ended'
195
+ elsif @sleep_timer; 'asleep'
196
+ elsif @mutex_asleep; 'mutex'
197
+ elsif @joined_on; "joined on job #{@joined_on.id}"
198
+ elsif @is_yielded; 'yielded'
199
+ elsif !@fiber; 'missing fiber'
200
+ elsif @fiber.alive?; 'alive'
201
+ else 'dead fiber'
202
+ end
203
+ end
204
+
205
+ def puts(msg)
206
+ Log.puts "[Job #@id] #{msg}"
207
+ end
208
+
209
+ def time_warning(name=nil)
210
+ raise "Job#time_warning called for job #@id while it is suspended" if yielded?
211
+ if @time_warning_started_at
212
+ duration = Actuator.now - @time_warning_started_at
213
+ @time_warning_extra ||= 0.0
214
+ @time_warning_extra += duration
215
+ (@time_warning_stack ||= []) << [@time_warning_name, @time_warning_extra]
216
+ end
217
+ @time_warning_extra = 0
218
+ if name
219
+ @time_warning_name = name
220
+ @time_warning_started_at = Actuator.now
221
+ else
222
+ @time_warning_name = nil
223
+ @time_warning_started_at = nil
224
+ end
225
+ duration
226
+ end
227
+
228
+ def time_warning!(name=nil)
229
+ raise "Job#time_warning! called for job #@id while it is suspended" if yielded?
230
+ if @time_warning_started_at
231
+ now = Actuator.now
232
+ duration = now - @time_warning_started_at + @time_warning_extra
233
+ if duration > 0.0025
234
+ Log.warn "Time warning: #@time_warning_name took #{(duration * 1000).round(3)} ms"
235
+ end
236
+ end
237
+ @time_warning_extra = 0
238
+ if name
239
+ @time_warning_name = name
240
+ @time_warning_started_at = Actuator.now
241
+ else
242
+ if @time_warning_stack && (outstanding = @time_warning_stack.shift)
243
+ @time_warning_name = outstanding[0]
244
+ @time_warning_extra = outstanding[1]
245
+ @time_warning_started_at = Actuator.now
246
+ else
247
+ @time_warning_name = nil
248
+ @time_warning_started_at = nil
249
+ end
250
+ end
251
+ duration
252
+ end
253
+ end
254
+ end
255
+
256
+ Job = Actuator::Job
@@ -0,0 +1,116 @@
1
+ module Actuator
2
+ class Mutex
3
+ def initialize
4
+ @waiters = []
5
+ end
6
+
7
+ def lock
8
+ job = Job.current
9
+ raise FiberError if @waiters.include? job
10
+ @waiters << job
11
+ if @waiters.size > 1
12
+ Job.yield
13
+ end
14
+ true
15
+ end
16
+
17
+ def locked?
18
+ !@waiters.empty?
19
+ end
20
+
21
+ def _wake_up(job)
22
+ if job.sleep_timer
23
+ job.wake!
24
+ elsif job.mutex_asleep
25
+ job.mutex_asleep = nil
26
+ job.fiber.resume
27
+ else
28
+ raise FiberError, "_wake_up called for job #{job.id} which is #{job.state}"
29
+ end
30
+ end
31
+
32
+ def sleep(timeout=nil)
33
+ unlock
34
+ if timeout
35
+ Job.sleep(timeout)
36
+ else
37
+ job = Job.current
38
+ job.mutex_asleep = self
39
+ begin
40
+ Job.yield
41
+ ensure
42
+ job.mutex_asleep = nil
43
+ end
44
+ end
45
+ nil
46
+ ensure
47
+ lock
48
+ end
49
+
50
+ def try_lock
51
+ lock unless locked?
52
+ end
53
+
54
+ def unlock
55
+ unless @waiters.first == Job.current
56
+ raise "[Job #{Job.current.id}] Mutex#unlock called from job which does not have the lock - @waiters: #{@waiters.map(&:id).inspect}"
57
+ end
58
+ @waiters.shift
59
+ unless @waiters.empty?
60
+ Actuator.next_tick do
61
+ next if @waiters.empty?
62
+ @waiters.first.fiber.resume
63
+ end
64
+ end
65
+ self
66
+ end
67
+
68
+ def synchronize
69
+ lock
70
+ yield
71
+ ensure
72
+ unlock
73
+ end
74
+ end
75
+
76
+ class ConditionVariable
77
+ def initialize
78
+ @waiters = []
79
+ end
80
+
81
+ def wait(mutex, timeout=nil)
82
+ job = Job.current
83
+ waiter = [mutex, job]
84
+ @waiters << waiter
85
+ mutex.sleep timeout
86
+ self
87
+ ensure
88
+ @waiters.delete waiter
89
+ end
90
+
91
+ def signal
92
+ while waiter = @waiters.shift
93
+ job = waiter[1]
94
+ next unless job.alive?
95
+ Actuator.next_tick do
96
+ waiter[0]._wake_up(job) if job.alive?
97
+ end
98
+ break
99
+ end
100
+ self
101
+ end
102
+
103
+ def broadcast
104
+ waiters = @waiters
105
+ @waiters = []
106
+ waiters.each do |waiter|
107
+ job = waiter[1]
108
+ next unless job.alive?
109
+ Actuator.next_tick do
110
+ waiter[0]._wake_up(job) if job.alive?
111
+ end
112
+ end
113
+ self
114
+ end
115
+ end
116
+ end