Line 0
Link Here
|
|
|
1 |
package Koha::Icarus::Task; |
2 |
|
3 |
use Modern::Perl; |
4 |
use POE qw(Wheel::Run); |
5 |
use Data::Dumper; |
6 |
use DateTime; |
7 |
use DateTime::Format::Strptime; |
8 |
use JSON; |
9 |
use Module::Load::Conditional qw/can_load/; |
10 |
|
11 |
my $datetime_pattern = DateTime::Format::Strptime->new( |
12 |
pattern => '%F %T', |
13 |
time_zone => 'local', |
14 |
); |
15 |
my $epoch_pattern = DateTime::Format::Strptime->new( |
16 |
pattern => '%s', |
17 |
); |
18 |
|
19 |
sub new { |
20 |
my ($class, $args) = @_; |
21 |
$args = {} unless defined $args; |
22 |
|
23 |
my $message = $args->{message}; |
24 |
my $server_id = $args->{server_id}; |
25 |
|
26 |
my $task_session = POE::Session->create( |
27 |
inline_states => { |
28 |
_start => sub { |
29 |
my ($session, $kernel, $heap, $task, $server_id) = @_[SESSION, KERNEL, HEAP, ARG0, ARG1]; |
30 |
$task->{status} = 'new'; |
31 |
#Trap terminal signals so that the task can stop gracefully. |
32 |
$kernel->sig(INT => "got_terminal_signal"); |
33 |
$kernel->sig(TERM => "got_terminal_signal"); |
34 |
|
35 |
my $session_id = $session->ID; |
36 |
if ($server_id){ |
37 |
$heap->{server_id} = $server_id; |
38 |
} |
39 |
|
40 |
#Tell the kernel that this task is waiting for an external action (ie keepalive counter) |
41 |
$kernel->refcount_increment($session_id,"waiting task"); |
42 |
$heap->{task} = $task; |
43 |
return $task; #FIXME: We probably shouldn't store $task in two places... that will get out of sync |
44 |
#Except that it's a reference... so it's all good |
45 |
}, |
46 |
"got_child_stdout" => \&on_child_stdout, |
47 |
"got_child_stderr" => \&on_child_stderr, |
48 |
"got_child_close" => \&on_child_close, |
49 |
"got_child_signal" => \&on_child_signal, |
50 |
"got_terminal_signal" => \&on_terminal_signal, |
51 |
"external_done" => \&external_done, |
52 |
"got_task_stop" => \&on_task_stop, |
53 |
"on_task_init" => \&on_task_init, |
54 |
"on_task_start" => \&on_task_start, |
55 |
}, |
56 |
args => [ $message->{task}, $server_id ], |
57 |
); |
58 |
return $task_session; |
59 |
} |
60 |
|
61 |
sub on_terminal_signal { |
62 |
my ($signal,$session,$kernel) = @_[ARG0,SESSION,KERNEL]; |
63 |
say "I've trapped the following signal: $signal."; |
64 |
$kernel->call($session, "got_task_stop"); |
65 |
} |
66 |
|
67 |
sub on_task_stop { |
68 |
my ($session, $kernel, $heap) = @_[SESSION, KERNEL, HEAP]; |
69 |
my $task = $heap->{task}; |
70 |
$task->{status} = 'stopping'; |
71 |
my $task_id = $session->ID; |
72 |
my $server_id = $heap->{server_id}; |
73 |
|
74 |
if ($heap->{stopping}){ |
75 |
say "$task_id: I'm already stopping! Be patient!"; |
76 |
|
77 |
} else { |
78 |
say "Task $task_id: I'm trying to stop myself now..."; |
79 |
|
80 |
#Mark this task as stopping |
81 |
$heap->{stopping} = 1; |
82 |
|
83 |
#Stop the task from spawning new jobs |
84 |
$kernel->alarm("on_task_start"); |
85 |
|
86 |
my $children_by_pid = $heap->{children_by_pid}; |
87 |
if ($children_by_pid && %$children_by_pid){ |
88 |
say "Task $task_id: I have child processes in progress..."; |
89 |
my $child_processes = $heap->{children_by_pid}; |
90 |
foreach my $child_pid (keys %$child_processes){ |
91 |
my $child = $child_processes->{$child_pid}; |
92 |
say "Task $task_id: I'm telling child pid $child_pid to stop"; |
93 |
$child->put("quit"); |
94 |
#TODO: Perhaps it would be worthwhile having a kill switch too? |
95 |
# my $rv = $child->kill("TERM"); |
96 |
} |
97 |
} |
98 |
info($task_id,"I'm also telling the kernel that I don't need to be kept alive anymore."); |
99 |
# say "Task $task_id: I'm also telling the kernel that I don't need to be kept alive anymore."; |
100 |
$kernel->refcount_decrement($task_id,"waiting task"); |
101 |
|
102 |
} |
103 |
} |
104 |
|
105 |
sub external_done { |
106 |
my ($heap,$session,$kernel) = @_[HEAP,SESSION,KERNEL]; |
107 |
|
108 |
say "External task is done... what now?"; |
109 |
#FIXME: remove this line |
110 |
# say "I think the server is ".$heap->{server_id}; |
111 |
|
112 |
if ($heap->{stopping}){ |
113 |
say "I'm stopping"; |
114 |
} else { |
115 |
my $task = $heap->{task}; |
116 |
if (my $repeat_interval = $task->{repeat_interval}){ |
117 |
#NOTE: Repeat the task |
118 |
$task->{status} = "restarting"; |
119 |
$kernel->yield("on_task_init"); |
120 |
} else { |
121 |
say "I'm going to stop this task"; |
122 |
$kernel->yield("got_task_stop"); |
123 |
} |
124 |
} |
125 |
|
126 |
} |
127 |
|
128 |
#This sub is just to start it now, or set it to start in the future... if the time is now or in the past, it starts now... if it's in the future, it starts in the future... |
129 |
sub on_task_init { |
130 |
my ($session, $kernel, $heap) = @_[SESSION, KERNEL, HEAP]; |
131 |
my $response = 'pending'; |
132 |
my $task = $heap->{task}; |
133 |
my $status = $task->{status}; |
134 |
if ($status){ |
135 |
if ($status eq 'started'){ |
136 |
$response = 'already started'; |
137 |
} elsif ($status eq 'pending'){ |
138 |
$response = 'already pending'; |
139 |
} else { |
140 |
$task->{status} = 'pending'; |
141 |
|
142 |
|
143 |
my $start = $task->{start}; |
144 |
if ( my $dt = $datetime_pattern->parse_datetime($start) ){ |
145 |
$start = $dt->epoch; |
146 |
} elsif ( $epoch_pattern->parse_datetime($start) ){ |
147 |
#No change required |
148 |
} else { |
149 |
#If we don't match the datetime_pattern or epoch_pattern, then we start right now. |
150 |
$start = time(); #time() returns a UNIX epoch time value |
151 |
} |
152 |
|
153 |
#FIXME: Make this log properly... |
154 |
say "Start task at $start"; |
155 |
#NOTE: $start must be in UNIX epoch time (ie number of seconds that have elapsed since 00:00:00 UTC Thursday 1 January 1970) |
156 |
$kernel->alarm("on_task_start",$start); |
157 |
} |
158 |
} |
159 |
return $response; |
160 |
} |
161 |
|
162 |
#This is where the magic will happen... |
163 |
sub on_task_start { |
164 |
my ($session, $kernel, $heap) = @_[SESSION, KERNEL, HEAP]; |
165 |
my $task = $heap->{task}; |
166 |
$task->{status} = 'started'; |
167 |
my $task_id = $session->ID; |
168 |
|
169 |
if (my $repeat_interval = $task->{repeat_interval}){ |
170 |
#NOTE: Reset the start time with a human readable timestamp |
171 |
my $dt = DateTime->now( time_zone => 'local', ); |
172 |
$dt->add( seconds => $repeat_interval ); |
173 |
$task->{start} = $dt->strftime("%F %T"); |
174 |
#$task->{start} = time() + $repeat_interval; |
175 |
} |
176 |
#FIXME: You need to impose child process limits here! How many child processes are allowed to be running at any given time? Well, you can only have one child process per task... |
177 |
#so it's really more of a limit on the number of tasks... you probably need to have an internal task queue... that's easy enough though. |
178 |
my $child = POE::Wheel::Run->new( |
179 |
ProgramArgs => [ $task, ], |
180 |
Program => sub { |
181 |
my ($task) = @_; |
182 |
|
183 |
|
184 |
#Shutdown the socket listener on the child process, so there's 0 chance of writing to or reading from the socket in the child process |
185 |
my $session = $poe_kernel->get_active_session(); |
186 |
my $heap = $session->get_heap(); |
187 |
$poe_kernel->call($heap->{server_id},"set_verbosity",0); #This turns off the server logging in this forked process |
188 |
$poe_kernel->call($heap->{server_id},"shutdown"); |
189 |
|
190 |
#FIXME: I don't know if this really needs to be run...but it probably doesn't hurt... |
191 |
$poe_kernel->stop(); |
192 |
|
193 |
|
194 |
my $task_type = $task->{type}; |
195 |
say "Task type = $task_type"; |
196 |
say "Start = $task->{start}"; |
197 |
say "Repeat interval = $task->{repeat_interval}"; |
198 |
use Data::Dumper; |
199 |
warn Dumper($task); |
200 |
if ( can_load ( modules => { $task_type => undef, }, ) ){ |
201 |
my $task_object = $task_type->new({task => $task}); |
202 |
if ($task_object){ |
203 |
#Synchronous action |
204 |
$task_object->run; |
205 |
} |
206 |
} else { |
207 |
die "Couldn't load module $task_type: $Module::Load::Conditional::ERROR" |
208 |
} |
209 |
|
210 |
#Handle them as you download them... which is going to be slow... unless you use AnyEvent::HTTP or POE::Component::Client::HTTP |
211 |
#Or... write them to disk and let an importer handle it! Write them to a specific directory... and then configure the importer to look in that directory with a certain profile... |
212 |
|
213 |
#FIXME: Ideally, I'd like to handle different data receivers... I'm going to use file:// protocol for now to write to disk, and think about other protocols... |
214 |
#Database would be one option, although like Disk... it's expensive |
215 |
#I wonder how well it would work writing to shared memory... I feel like that could be problematic with large amounts of data... |
216 |
#For now... why don't you just stick to writing to disk... |
217 |
|
218 |
#FIXME: Importer... I think the Importer (Daedalus?) will periodically check the staging area? |
219 |
#FIXME: Will that be too slow though? I suppose you could signal the importer? |
220 |
#FIXME: You could have it checking every second though... that would be trivial I think. Like the repeat for the harvesting really... |
221 |
#You could have the daemon checking every second, and every time it finds something, it could spin up a child worker process to handle things up to a limit of X workers... and then it slows down. |
222 |
# |
223 |
|
224 |
}, |
225 |
StdoutEvent => "got_child_stdout", |
226 |
StderrEvent => "got_child_stderr", |
227 |
CloseEvent => "got_child_close", |
228 |
NoSetPgrp => 1, #Keep child processes in same group as parent |
229 |
); |
230 |
|
231 |
$_[KERNEL]->sig_child($child->PID, "got_child_signal"); |
232 |
# Wheel events include the wheel's ID. |
233 |
$_[HEAP]{children_by_wid}{$child->ID} = $child; |
234 |
# Signal events include the process ID. |
235 |
$_[HEAP]{children_by_pid}{$child->PID} = $child; |
236 |
|
237 |
info($task_id,"child pid ".$child->PID." started as wheel ".$child->ID); |
238 |
} |
239 |
|
240 |
|
241 |
# These methods are for communicating with child processes... |
242 |
# "on_child_*" events are standard from POE::Wheel::Run example |
243 |
# |
244 |
|
245 |
# Wheel event, including the wheel's ID |
246 |
sub on_child_stdout { |
247 |
my ($stdout_line, $wheel_id, $session) = @_[ARG0, ARG1, SESSION]; |
248 |
|
249 |
my $now = DateTime->now(); |
250 |
|
251 |
|
252 |
my $task_id = $session->ID; |
253 |
my $child = $_[HEAP]{children_by_wid}{$wheel_id}; |
254 |
print "[$now] [task $task_id] [pid ".$child->PID."] STDOUT: $stdout_line\n"; |
255 |
if ($stdout_line =~ /^UPDATE_PARAMS=(.*)$/){ |
256 |
my $json_string = $1; |
257 |
my $json = from_json($json_string); |
258 |
my $task = $_[HEAP]->{task}; |
259 |
my $params = $task->{params}; |
260 |
foreach my $key (%$json){ |
261 |
if (defined $params->{$key}){ |
262 |
#FIXME: Don't just overwrite? Only update differences? |
263 |
$params->{$key} = $json->{$key}; |
264 |
} |
265 |
} |
266 |
$_[HEAP]->{task} = $task; |
267 |
} |
268 |
} |
269 |
|
270 |
# Wheel event, including the wheel's ID. |
271 |
sub on_child_stderr { |
272 |
my ($stderr_line, $wheel_id, $session) = @_[ARG0, ARG1,SESSION]; |
273 |
my $now = DateTime->now(); |
274 |
my $task_id = $session->ID; |
275 |
my $child = $_[HEAP]{children_by_wid}{$wheel_id}; |
276 |
print "[$now] [task $task_id] [pid ".$child->PID."] STDERR: $stderr_line\n"; |
277 |
} |
278 |
|
279 |
# Wheel event, including the wheel's ID. |
280 |
sub on_child_close { |
281 |
my ($wheel_id,$session,$kernel) = @_[ARG0,SESSION,KERNEL]; |
282 |
my $now = DateTime->now(); |
283 |
|
284 |
my $task_id = $session->ID; |
285 |
my $child = delete $_[HEAP]{children_by_wid}{$wheel_id}; |
286 |
|
287 |
# May have been reaped by on_child_signal(). |
288 |
unless (defined $child) { |
289 |
info($task_id,"[wid $wheel_id] closed all pipes."); |
290 |
return; |
291 |
} |
292 |
info($task_id,"[pid ".$child->PID."] closed all pipes."); |
293 |
delete $_[HEAP]{children_by_pid}{$child->PID}; |
294 |
|
295 |
} |
296 |
|
297 |
sub on_child_signal { |
298 |
my ($heap,$kernel,$pid,$exit_code,$session) = @_[HEAP,KERNEL,ARG1,ARG2,SESSION]; |
299 |
my $now = DateTime->now(); |
300 |
my $task_id = $session->ID; |
301 |
#FIXME: Tell the session that it's ready to do the next thing... |
302 |
if ($exit_code == 0){ |
303 |
$kernel->yield("external_done"); |
304 |
} |
305 |
info($task_id,"pid $pid exited with status $exit_code."); |
306 |
my $child = delete $_[HEAP]{children_by_pid}{$pid}; |
307 |
|
308 |
|
309 |
|
310 |
# May have been reaped by on_child_close(). |
311 |
return unless defined $child; |
312 |
|
313 |
delete $_[HEAP]{children_by_wid}{$child->ID}; |
314 |
} |
315 |
|
316 |
sub info { |
317 |
my $task_id = shift; |
318 |
my $message = shift; |
319 |
my $now = DateTime->now(time_zone => "local"); |
320 |
say "[$now] [task $task_id] $message"; |
321 |
} |
322 |
|
323 |
|
324 |
1; |