Line 0
Link Here
|
|
|
1 |
package Koha::Icarus::Task; |
2 |
|
3 |
use Modern::Perl; |
4 |
use parent 'Koha::Icarus::Base'; |
5 |
|
6 |
use POE qw(Wheel::Run); |
7 |
use DateTime; |
8 |
use DateTime::Format::Strptime; |
9 |
use JSON; |
10 |
use Module::Load::Conditional qw/can_load/; |
11 |
|
12 |
my $datetime_pattern = DateTime::Format::Strptime->new( |
13 |
pattern => '%F %T', |
14 |
time_zone => 'local', |
15 |
); |
16 |
my $epoch_pattern = DateTime::Format::Strptime->new( |
17 |
pattern => '%s', |
18 |
); |
19 |
|
20 |
sub new { |
21 |
my ($class, $args) = @_; |
22 |
$args = {} unless defined $args; |
23 |
$args->{_component} = "task"; |
24 |
$args->{_id} = "undefined"; |
25 |
return bless ($args, $class); |
26 |
} |
27 |
|
28 |
#NOTE: "spawn" inspired by http://poe.perl.org/?POE_Cookbook/Object_Methods |
29 |
sub spawn { |
30 |
my ($class, $args) = @_; |
31 |
my $self = $class->new($args); |
32 |
my $task_session = POE::Session->create( |
33 |
object_states => [ |
34 |
$self => { |
35 |
_start => "on_task_create", |
36 |
"got_child_stdout" => "on_child_stdout", |
37 |
"got_child_stderr" => "on_child_stderr", |
38 |
"got_child_close" => "on_child_close", |
39 |
"got_child_signal" => "on_child_signal", |
40 |
"got_terminal_signal" => "on_terminal_signal", |
41 |
"child_process_success" => "child_process_success", |
42 |
"child_process_failure" => "child_process_failure", |
43 |
"got_task_stop" => "on_task_stop", |
44 |
"on_task_init" => "on_task_init", |
45 |
"on_task_start" => "on_task_start", |
46 |
}, |
47 |
], |
48 |
); |
49 |
return $task_session; |
50 |
} |
51 |
|
52 |
sub on_task_create { |
53 |
my ($self, $session, $kernel, $heap) = @_[OBJECT, SESSION, KERNEL, HEAP]; |
54 |
|
55 |
#Trap terminal signals so that the task can stop gracefully. |
56 |
$kernel->sig(INT => "got_terminal_signal"); |
57 |
$kernel->sig(TERM => "got_terminal_signal"); |
58 |
|
59 |
my $task_id = $session->ID; |
60 |
if ($task_id){ |
61 |
#Tell the kernel that this task is waiting for an external action (ie keepalive counter) |
62 |
$kernel->refcount_increment($task_id,"waiting task"); |
63 |
$self->{_id} = $task_id; #Set internal id for logging purposes |
64 |
} |
65 |
|
66 |
my $server_id = $self->{server_id}; |
67 |
if ($server_id){ |
68 |
$heap->{server_id} = $server_id; |
69 |
} |
70 |
|
71 |
my $task = undef; |
72 |
my $message = $self->{message}; |
73 |
if ($message){ |
74 |
$task = $message->{task}; |
75 |
if ($task){ |
76 |
$task->{status} = 'new'; |
77 |
$heap->{task} = $task; |
78 |
} |
79 |
} |
80 |
return $task; #This return value is used by the parent POE session's _child handler |
81 |
} |
82 |
|
83 |
#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... |
84 |
sub on_task_init { |
85 |
my ($self, $session, $kernel, $heap) = @_[OBJECT, SESSION, KERNEL, HEAP]; |
86 |
my $response = 'pending'; |
87 |
my $task = $heap->{task}; |
88 |
my $status = $task->{status}; |
89 |
if ($status){ |
90 |
if ($status eq 'started'){ |
91 |
$response = 'already started'; |
92 |
} elsif ($status eq 'pending'){ |
93 |
$response = 'already pending'; |
94 |
} else { |
95 |
$task->{status} = 'pending'; |
96 |
|
97 |
my $start = $task->{start}; |
98 |
my $start_message = $start; |
99 |
|
100 |
|
101 |
my $dt; |
102 |
if ( $dt = $datetime_pattern->parse_datetime($start) ){ |
103 |
#e.g. 2016-04-06 00:00:00 |
104 |
} elsif ( $dt = $epoch_pattern->parse_datetime($start) ){ |
105 |
#e.g. 1459837498 or apparently 0000-00-00 00:00:00 |
106 |
} else { |
107 |
#If we don't match the datetime_pattern or epoch_pattern, then we start right now. |
108 |
$dt = DateTime->now( time_zone => 'local', ); |
109 |
} |
110 |
if ($dt){ |
111 |
$start = $dt->epoch; |
112 |
$start_message = $dt; |
113 |
} |
114 |
|
115 |
|
116 |
$self->log("Start task at $start_message"); |
117 |
#NOTE: $start must be in UNIX epoch time (ie number of seconds that have elapsed since 00:00:00 UTC Thursday 1 January 1970) |
118 |
$kernel->alarm("on_task_start",$start); |
119 |
} |
120 |
} |
121 |
return $response; |
122 |
} |
123 |
|
124 |
sub on_task_start { |
125 |
my ($self, $session, $kernel, $heap) = @_[OBJECT, SESSION, KERNEL, HEAP]; |
126 |
my $task = $heap->{task}; |
127 |
$task->{status} = 'started'; |
128 |
|
129 |
if (my $repeat_interval = $task->{repeat_interval}){ |
130 |
#NOTE: Reset the start time with a human readable timestamp |
131 |
my $dt = DateTime->now( time_zone => 'local', ); |
132 |
$dt->add( seconds => $repeat_interval ); |
133 |
$task->{start} = $dt->strftime("%F %T"); |
134 |
} |
135 |
#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... |
136 |
#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. |
137 |
my $child = POE::Wheel::Run->new( |
138 |
ProgramArgs => [ $task, ], |
139 |
Program => sub { |
140 |
my ($task) = @_; |
141 |
|
142 |
#Perform some last minute POE calls before running the task module plugin |
143 |
my $session = $poe_kernel->get_active_session(); |
144 |
if ($session){ |
145 |
my $heap = $session->get_heap(); |
146 |
$poe_kernel->call($heap->{server_id},"set_verbosity",0); #This turns off the server logging in this forked process, so the following call() doesn't mess up our logs |
147 |
$poe_kernel->call($heap->{server_id},"shutdown"); #Shutdown the socket listener on the child process, so there's zero chance of writing to or reading from the socket in the child process |
148 |
} |
149 |
|
150 |
#NOTE: I don't know if this really needs to be run, but it shouldn't hurt. |
151 |
$poe_kernel->stop(); |
152 |
|
153 |
#Try to load the task type module. |
154 |
my $task_type = $task->{type}; |
155 |
if ( can_load ( modules => { $task_type => undef, }, ) ){ |
156 |
#Create the object |
157 |
my $task_object = $task_type->new({task => $task, Verbosity => $self->{Verbosity}, }); |
158 |
if ($task_object){ |
159 |
#Synchronous action: run the task module |
160 |
$task_object->run; |
161 |
} |
162 |
} else { |
163 |
die "Couldn't load module $task_type: $Module::Load::Conditional::ERROR" |
164 |
} |
165 |
}, |
166 |
StdoutEvent => "got_child_stdout", |
167 |
StderrEvent => "got_child_stderr", |
168 |
CloseEvent => "got_child_close", |
169 |
NoSetPgrp => 1, #Keep child processes in same group as parent. This is especially useful when using Ctrl+C to kill the whole group. |
170 |
); |
171 |
|
172 |
$kernel->sig_child($child->PID, "got_child_signal"); |
173 |
# Wheel events include the wheel's ID. |
174 |
$_[HEAP]{children_by_wid}{$child->ID} = $child; |
175 |
# Signal events include the process ID. |
176 |
$_[HEAP]{children_by_pid}{$child->PID} = $child; |
177 |
|
178 |
$self->debug("child pid ".$child->PID." started as wheel ".$child->ID); |
179 |
} |
180 |
|
181 |
sub on_task_stop { |
182 |
my ($self, $session, $kernel, $heap) = @_[OBJECT, SESSION, KERNEL, HEAP]; |
183 |
my $task = $heap->{task}; |
184 |
$task->{status} = 'stopping'; |
185 |
my $task_id = $session->ID; |
186 |
my $server_id = $heap->{server_id}; |
187 |
|
188 |
if ($heap->{stopping}){ |
189 |
$self->debug("Task is already in the process of stopping..."); |
190 |
|
191 |
} else { |
192 |
|
193 |
$self->log("Trying to stop task."); |
194 |
|
195 |
|
196 |
#Mark this task as stopping |
197 |
$heap->{stopping} = 1; |
198 |
|
199 |
#Stop the task from spawning new jobs |
200 |
$kernel->alarm("on_task_start"); |
201 |
|
202 |
my $children_by_pid = $heap->{children_by_pid}; |
203 |
if ($children_by_pid && %$children_by_pid){ |
204 |
|
205 |
$self->debug("Child processes in progres..."); |
206 |
my $child_processes = $heap->{children_by_pid}; |
207 |
foreach my $child_pid (keys %$child_processes){ |
208 |
my $child = $child_processes->{$child_pid}; |
209 |
$self->debug("Telling child pid $child_pid to stop"); |
210 |
$child->put("quit"); |
211 |
#TODO: Perhaps it would be worthwhile having a kill switch too? |
212 |
# my $rv = $child->kill("TERM"); |
213 |
} |
214 |
} |
215 |
|
216 |
$self->log("Removing task keepalive."); |
217 |
|
218 |
$kernel->refcount_decrement($task_id,"waiting task"); |
219 |
} |
220 |
} |
221 |
|
222 |
sub on_terminal_signal { |
223 |
my ($self, $signal,$session,$kernel) = @_[OBJECT, ARG0,SESSION,KERNEL]; |
224 |
$self->debug("Trapped SIGNAL: $signal."); |
225 |
#Gracefully stop the task |
226 |
$kernel->call($session, "got_task_stop"); |
227 |
} |
228 |
|
229 |
sub child_process_failure { |
230 |
my ($self, $heap,$session,$kernel) = @_[OBJECT, HEAP,SESSION,KERNEL]; |
231 |
my $task = $heap->{task}; |
232 |
$task->{status} = "failed"; |
233 |
} |
234 |
|
235 |
sub child_process_success { |
236 |
my ($self, $heap,$session,$kernel) = @_[OBJECT, HEAP,SESSION,KERNEL]; |
237 |
my $task = $heap->{task}; |
238 |
if (my $repeat_interval = $task->{repeat_interval}){ |
239 |
if ($heap->{stopping}){ |
240 |
$self->log("Will skip repeating the task, as task is stopping."); |
241 |
} else { |
242 |
$self->log("Will repeat the task"); |
243 |
$task->{status} = "restarting"; |
244 |
$kernel->yield("on_task_init"); |
245 |
} |
246 |
} else { |
247 |
$self->debug("I'm going to stop this task"); |
248 |
$kernel->yield("got_task_stop"); |
249 |
} |
250 |
} |
251 |
|
252 |
############################################################# |
253 |
# # |
254 |
# Methods for communicating with child processes # |
255 |
# # |
256 |
############################################################# |
257 |
# Originally inspired by the POE::Wheel::Run perldoc example |
258 |
|
259 |
# Wheel event, including the wheel's ID |
260 |
sub on_child_stdout { |
261 |
my ($self, $stdout_line, $wheel_id, $session) = @_[OBJECT, ARG0, ARG1, SESSION]; |
262 |
my $child = $_[HEAP]{children_by_wid}{$wheel_id}; |
263 |
#NOTE: Log everything child process sends to STDOUT |
264 |
$self->log("[pid ".$child->PID."] STDOUT: $stdout_line"); |
265 |
|
266 |
#If the child outputs a line to STDOUT which starts with UPDATE_PARAMS=, we capture the data, |
267 |
#and update the task params. |
268 |
if ($stdout_line =~ /^UPDATE_PARAMS=(.*)$/){ |
269 |
my $json_string = $1; |
270 |
my $json = from_json($json_string); |
271 |
my $task = $_[HEAP]->{task}; |
272 |
my $params = $task->{params}; |
273 |
foreach my $key (%$json){ |
274 |
if (defined $params->{$key}){ |
275 |
#FIXME: Don't just overwrite? Only update differences? |
276 |
$params->{$key} = $json->{$key}; |
277 |
} |
278 |
} |
279 |
$_[HEAP]->{task} = $task; |
280 |
} |
281 |
} |
282 |
|
283 |
# Wheel event, including the wheel's ID. |
284 |
sub on_child_stderr { |
285 |
my ($self, $stderr_line, $wheel_id, $session) = @_[OBJECT, ARG0, ARG1,SESSION]; |
286 |
my $child = $_[HEAP]{children_by_wid}{$wheel_id}; |
287 |
#NOTE: Log everything child process sends to STDERR |
288 |
$self->log("[pid ".$child->PID."] STDERR: $stderr_line"); |
289 |
} |
290 |
|
291 |
# Wheel event, including the wheel's ID. |
292 |
sub on_child_close { |
293 |
my ($self, $wheel_id,$session,$kernel) = @_[OBJECT, ARG0,SESSION,KERNEL]; |
294 |
|
295 |
my $child = delete $_[HEAP]{children_by_wid}{$wheel_id}; |
296 |
|
297 |
# May have been reaped by on_child_signal(). |
298 |
unless (defined $child) { |
299 |
$self->debug("[wid $wheel_id] closed all pipes."); |
300 |
return; |
301 |
} |
302 |
$self->debug("[pid ".$child->PID."] closed all pipes."); |
303 |
delete $_[HEAP]{children_by_pid}{$child->PID}; |
304 |
} |
305 |
|
306 |
sub on_child_signal { |
307 |
my ($self, $heap,$kernel,$pid,$exit_code,$session) = @_[OBJECT, HEAP,KERNEL,ARG1,ARG2,SESSION]; |
308 |
|
309 |
#If the child's exit code is 0, handle this successful exit status |
310 |
if ($exit_code == 0){ |
311 |
$kernel->yield("child_process_success"); |
312 |
} else { |
313 |
$kernel->yield("child_process_failure"); |
314 |
} |
315 |
$self->debug("pid $pid exited with status $exit_code."); |
316 |
my $child = delete $_[HEAP]{children_by_pid}{$pid}; |
317 |
|
318 |
# May have been reaped by on_child_close(). |
319 |
return unless defined $child; |
320 |
|
321 |
delete $_[HEAP]{children_by_wid}{$child->ID}; |
322 |
} |
323 |
|
324 |
1; |