actuator 0.0.1

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