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.
- checksums.yaml +7 -0
- data/.autotest +13 -0
- data/History.txt +3 -0
- data/Manifest.txt +28 -0
- data/README.md +105 -0
- data/Rakefile +23 -0
- data/ext/actuator/actuator.c +2 -0
- data/ext/actuator/actuator.h +16 -0
- data/ext/actuator/clock.c +81 -0
- data/ext/actuator/clock.h +15 -0
- data/ext/actuator/debug.c +4 -0
- data/ext/actuator/debug.h +56 -0
- data/ext/actuator/extconf.rb +5 -0
- data/ext/actuator/log.cpp +134 -0
- data/ext/actuator/log.h +18 -0
- data/ext/actuator/reactor.cpp +212 -0
- data/ext/actuator/reactor.h +34 -0
- data/ext/actuator/ruby_helpers.c +17 -0
- data/ext/actuator/ruby_helpers.h +17 -0
- data/ext/actuator/timer.cpp +450 -0
- data/ext/actuator/timer.h +45 -0
- data/lib/actuator.rb +22 -0
- data/lib/actuator/fiber.rb +14 -0
- data/lib/actuator/fiber_pool.rb +82 -0
- data/lib/actuator/job.rb +256 -0
- data/lib/actuator/mutex.rb +116 -0
- data/lib/actuator/mutex/replace.rb +8 -0
- data/test/setup_test.rb +63 -0
- data/test/test_actuator.rb +90 -0
- metadata +134 -0
@@ -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
|
+
};
|
data/lib/actuator.rb
ADDED
@@ -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,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
|
data/lib/actuator/job.rb
ADDED
@@ -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
|