actuator 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|