1 package OpenILS::Application::Trigger;
2 use strict; use warnings;
3 use OpenILS::Application;
4 use base qw/OpenILS::Application/;
6 use OpenSRF::EX qw/:try/;
7 use OpenSRF::Utils::JSON;
9 use OpenSRF::AppSession;
10 use OpenSRF::MultiSession;
11 use OpenSRF::Utils::SettingsClient;
12 use OpenSRF::Utils::Logger qw/$logger/;
13 use OpenSRF::Utils qw/:datetime/;
16 use DateTime::Format::ISO8601;
18 use OpenILS::Utils::Fieldmapper;
19 use OpenILS::Utils::CStoreEditor q/:funcs/;
20 use OpenILS::Application::Trigger::Event;
21 use OpenILS::Application::Trigger::EventGroup;
24 my $log = 'OpenSRF::Utils::Logger';
30 my $conf = OpenSRF::Utils::SettingsClient->new;
31 $parallel_collect = $conf->config_value( apps => 'open-ils.trigger' => app_settings => parallel => 'collect') || 1;
32 $parallel_react = $conf->config_value( apps => 'open-ils.trigger' => app_settings => parallel => 'react') || 1;
37 sub create_active_events_for_object {
43 my $granularity = shift;
44 my $user_data = shift;
46 my $ident = $target->Identity;
47 my $ident_value = $target->$ident();
49 my $editor = new_editor(xact=>1);
51 my $hooks = $editor->search_action_trigger_hook(
53 core_type => $target->json_hint
62 my %hook_hash = map { ($_->key, $_) } @$hooks;
64 my $orgs = $editor->json_query({ from => [ 'actor.org_unit_ancestors' => $location ] });
65 my $defs = $editor->search_action_trigger_event_definition(
66 { hook => [ keys %hook_hash ],
67 owner => [ map { $_->{id} } @$orgs ],
72 for my $def ( @$defs ) {
73 next if ($granularity && $def->granularity ne $granularity );
75 if (!$self->{ignore_opt_in} && $def->usr_field && $def->opt_in_setting) {
76 my $ufield = $def->usr_field;
77 my $uid = $target->$ufield;
78 $uid = $uid->id if (ref $uid); # fleshed user object, unflesh it
80 my $opt_in_setting = $editor->search_actor_user_setting(
82 name => $def->opt_in_setting,
87 next unless (@$opt_in_setting);
90 my $date = DateTime->now;
92 if ($hook_hash{$def->hook}->passive eq 'f') {
94 if (my $dfield = $def->delay_field) {
95 if ($target->$dfield()) {
96 $date = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($target->$dfield) );
102 $date->add( seconds => interval_to_seconds($def->delay) );
105 my $event = Fieldmapper::action_trigger::event->new();
106 $event->target( $ident_value );
107 $event->event_def( $def->id );
108 $event->run_time( $date->strftime( '%F %T%z' ) );
109 $event->user_data( OpenSRF::Utils::JSON->perl2JSON($user_data) ) if (defined($user_data));
111 $editor->create_action_trigger_event( $event );
113 $client->respond( $event->id );
120 __PACKAGE__->register_method(
121 api_name => 'open-ils.trigger.event.autocreate',
122 method => 'create_active_events_for_object',
127 __PACKAGE__->register_method(
128 api_name => 'open-ils.trigger.event.autocreate.ignore_opt_in',
129 method => 'create_active_events_for_object',
136 sub create_event_for_object_and_def {
139 my $definitions = shift;
141 my $location = shift;
142 my $user_data = shift;
144 my $ident = $target->Identity;
145 my $ident_value = $target->$ident();
147 my @active = ($self->api_name =~ /inactive/o) ? () : ( active => 't' );
149 my $editor = new_editor(xact=>1);
151 my $orgs = $editor->json_query({ from => [ 'actor.org_unit_ancestors' => $location ] });
152 my $defs = $editor->search_action_trigger_event_definition(
153 { id => $definitions,
154 owner => [ map { $_->{id} } @$orgs ],
159 my $hooks = $editor->search_action_trigger_hook(
160 { key => [ map { $_->hook } @$defs ],
161 core_type => $target->json_hint
165 my %hook_hash = map { ($_->key, $_) } @$hooks;
167 for my $def ( @$defs ) {
169 if ($def->usr_field && $def->opt_in_setting) {
170 my $ufield = $def->usr_field;
171 my $uid = $target->$ufield;
172 $uid = $uid->id if (ref $uid); # fleshed user object, unflesh it
174 my $opt_in_setting = $editor->search_actor_user_setting(
176 name => $def->opt_in_setting,
181 next unless (@$opt_in_setting);
184 my $date = DateTime->now;
186 if ($hook_hash{$def->hook}->passive eq 'f') {
188 if (my $dfield = $def->delay_field) {
189 if ($target->$dfield()) {
190 $date = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($target->$dfield) );
196 $date->add( seconds => interval_to_seconds($def->delay) );
199 my $event = Fieldmapper::action_trigger::event->new();
200 $event->target( $ident_value );
201 $event->event_def( $def->id );
202 $event->run_time( $date->strftime( '%F %T%z' ) );
203 $event->user_data( OpenSRF::Utils::JSON->perl2JSON($user_data) ) if (defined($user_data));
205 $editor->create_action_trigger_event( $event );
207 $client->respond( $event->id );
214 __PACKAGE__->register_method(
215 api_name => 'open-ils.trigger.event.autocreate.by_definition',
216 method => 'create_event_for_object_and_def',
221 __PACKAGE__->register_method(
222 api_name => 'open-ils.trigger.event.autocreate.by_definition.include_inactive',
223 method => 'create_event_for_object_and_def',
230 # Retrieves events by object, or object type + filter
231 # $object : a target object or object type (class hint)
233 # $filter : an optional hash of filters ... top level keys:
235 # filters on the atev objects, such as states or null-ness of timing
236 # fields. contains the effective default of:
237 # { state => 'pending' }
238 # an example, which overrides the default, and will find
239 # stale 'found' events:
240 # { state => 'found', update_time => { '<' => 'yesterday' } }
243 # filters on the atevdef object. contains the effective default of:
247 # filters on the hook object. no defaults, but there is a pinned,
248 # unchangeable filter based on the passed hint or object type (see
249 # $object above). an example for finding passive events:
253 # filters against the target field on the event. this can contain
254 # either an array of target ids (if you passed an object type, and
255 # not an object) or can contain a json_query that will return exactly
256 # a list of target-type ids. If you pass an object, the pkey value of
257 # that object will be used as a filter in addition to the filter passed
258 # in here. example filter for circs of user 1234 that are open:
259 # { select => { circ => ['id'] },
263 # checkin_time => undef,
265 # { stop_fines => undef },
266 # { stop_fines => { 'not in' => ['LOST','LONGOVERDUE','CLAIMSRETURNED'] } }
270 sub events_by_target {
274 my $filter = shift || {};
275 my $flesh_fields = shift || {};
276 my $flesh_depth = shift || 1;
278 my $obj_class = ref($object) || _fm_class_by_hint($object);
279 my $obj_hint = ref($object) ? _fm_hint_by_class(ref($object)) : $object;
281 my $object_ident_field = $obj_class->Identity;
284 select => { atev => ["id"] },
291 ath => { field => "key", fkey => "hook" }
297 "+ath" => { core_type => $obj_hint },
298 "+atevdef" => { active => 't' },
299 "+atev" => { state => 'pending' }
301 order_by => { "atev" => [ 'run_time', 'add_time' ] }
304 $query->{limit} = $filter->{limit} if defined $filter->{limit};
305 $query->{offset} = $filter->{offset} if defined $filter->{offset};
306 $query->{order_by} = $filter->{order_by} if defined $filter->{order_by};
309 # allow multiple 'target' filters
310 $query->{where}->{'+atev'}->{'-and'} = [];
312 # if we got a real object, filter on its pkey value
313 if (ref($object)) { # pass an object, require that target
314 push @{ $query->{where}->{'+atev'}->{'-and'} },
315 { target => $object->$object_ident_field }
318 # we have a fancy complex target filter or a list of target ids
319 if ($$filter{target}) {
320 push @{ $query->{where}->{'+atev'}->{'-and'} },
321 { target => {in => $$filter{target} } };
324 # pass no target filter or object, you get no events
325 if (!@{ $query->{where}->{'+atev'}->{'-and'} }) {
329 # any hook filters, other than the required core_type filter
330 if ($$filter{hook}) {
331 $query->{where}->{'+ath'}->{$_} = $$filter{hook}{$_}
332 for (grep { $_ ne 'core_type' } keys %{$$filter{hook}});
335 # any event_def filters. defaults to { active => 't' }
336 if ($$filter{event_def}) {
337 $query->{where}->{'+atevdef'}->{$_} = $$filter{event_def}{$_}
338 for (keys %{$$filter{event_def}});
341 # any event filters. defaults to { state => 'pending' }.
342 # don't overwrite '-and' used for multiple target filters above
343 if ($$filter{event}) {
344 $query->{where}->{'+atev'}->{$_} = $$filter{event}{$_}
345 for (grep { $_ ne '-and' } keys %{$$filter{event}});
348 my $e = new_editor(xact=>1);
350 my $events = $e->json_query($query);
352 $flesh_fields->{atev} = ['event_def'] unless $flesh_fields->{atev};
354 for my $id (@$events) {
355 my $event = $e->retrieve_action_trigger_event([
357 {flesh => $flesh_depth, flesh_fields => $flesh_fields}
360 (my $meth = $obj_class) =~ s/^Fieldmapper:://o;
362 $meth = 'retrieve_'.$meth;
364 $event->target($e->$meth($event->target));
365 $client->respond($event);
370 __PACKAGE__->register_method(
371 api_name => 'open-ils.trigger.events_by_target',
372 method => 'events_by_target',
378 sub _fm_hint_by_class {
380 return OpenILS::Application->publish_fieldmapper->{$class}->{hint};
383 sub _fm_class_by_hint {
387 OpenILS::Application->publish_fieldmapper->{$_}->{hint} eq $hint
388 } keys %{ OpenILS::Application->publish_fieldmapper };
393 sub create_batch_events {
397 my $location_field = shift; # where to look for event_def.owner filtering ... circ_lib, for instance, where hook.core_type = circ
398 my $filter = shift || {};
399 my $granularity = shift;
400 my $user_data = shift;
402 my $active = ($self->api_name =~ /active/o) ? 1 : 0;
403 if ($active && !keys(%$filter)) {
404 $log->info("Active batch event creation requires a target filter but none was supplied to create_batch_events");
408 return undef unless ($key && $location_field);
410 my $editor = new_editor(xact=>1);
411 my $hooks = $editor->search_action_trigger_hook(
412 { passive => $active ? 'f' : 't', key => $key }
415 my %hook_hash = map { ($_->key, $_) } @$hooks;
417 my $defs = $editor->search_action_trigger_event_definition(
418 { hook => [ keys %hook_hash ], active => 't' },
421 my $orig_filter_and = [];
422 if ($$filter{'-and'}) {
423 for my $f ( @{ $$filter{'-and'} } ) {
424 push @$orig_filter_and, $f;
428 for my $def ( @$defs ) {
429 next if ($granularity && $def->granularity ne $granularity );
431 my $date = DateTime->now->subtract( seconds => interval_to_seconds($def->delay) );
433 # we may need to do some work to backport this to 1.2
434 $filter->{ $location_field } = { 'in' =>
436 select => { aou => [{ column => 'id', transform => 'actor.org_unit_descendants', result_field => 'id' }] },
438 where => { id => $def->owner }
442 my $run_time = 'now';
447 ->add( seconds => interval_to_seconds($def->delay) )
448 ->strftime( '%F %T%z' );
450 if ($def->max_delay) {
451 my @times = sort {$a <=> $b} interval_to_seconds($def->delay), interval_to_seconds($def->max_delay);
452 $filter->{ $def->delay_field } = {
454 DateTime->now->subtract( seconds => $times[1] )->strftime( '%F %T%z' ),
455 DateTime->now->subtract( seconds => $times[0] )->strftime( '%F %T%z' )
459 $filter->{ $def->delay_field } = {
460 '<=' => DateTime->now->subtract( seconds => interval_to_seconds($def->delay) )->strftime( '%F %T%z' )
465 my $class = _fm_class_by_hint($hook_hash{$def->hook}->core_type);
467 # filter where this target has an event (and it's pending, for active hooks)
468 $$filter{'-and'} = [];
469 for my $f ( @$orig_filter_and ) {
470 push @{ $$filter{'-and'} }, $f;
473 my $join = { 'join' => {
476 fkey => $class->Identity,
478 filter => { event_def => $def->id }
481 if ($def->repeat_delay) {
482 $join->{'join'}{atev}{filter} = { start_time => {
483 '>' => DateTime->now->subtract( seconds => interval_to_seconds($def->repeat_delay) )->strftime( '%F %T%z' )
487 push @{ $filter->{'-and'} }, { '+atev' => { id => undef } };
489 if ($def->usr_field && $def->opt_in_setting) {
490 push @{ $filter->{'-and'} }, {
494 name => $def->opt_in_setting,
495 usr => { '=' => { '+' . $hook_hash{$def->hook}->core_type => $def->usr_field } },
502 $class =~ s/^Fieldmapper:://o;
504 my $method = 'search_'. $class;
506 # for cleaner logging
507 my $def_id = $def->id;
508 my $hook = $def->hook;
510 $logger->info("trigger: create_batch_events() collecting object IDs for def=$def_id / hook=$hook");
512 my $object_ids = $editor->$method( [$filter, $join], {idlist => 1, timeout => 10800} );
515 $logger->info("trigger: create_batch_events() fetched ".scalar(@$object_ids)." object IDs for def=$def_id / hook=$hook");
517 $logger->warn("trigger: create_batch_events() timeout occurred collecting object IDs for def=$def_id / hook=$hook");
520 for my $o_id (@$object_ids) {
522 my $event = Fieldmapper::action_trigger::event->new();
523 $event->target( $o_id );
524 $event->event_def( $def->id );
525 $event->run_time( $run_time );
526 $event->user_data( OpenSRF::Utils::JSON->perl2JSON($user_data) ) if (defined($user_data));
528 $editor->create_action_trigger_event( $event );
530 $client->respond( $event->id );
533 $logger->info("trigger: create_batch_events() successfully created events for def=$def_id / hook=$hook");
536 $logger->info("trigger: create_batch_events() done creating events");
542 __PACKAGE__->register_method(
543 api_name => 'open-ils.trigger.passive.event.autocreate.batch',
544 method => 'create_batch_events',
550 __PACKAGE__->register_method(
551 api_name => 'open-ils.trigger.active.event.autocreate.batch',
552 method => 'create_batch_events',
558 sub fire_single_event {
561 my $event_id = shift;
563 my $e = OpenILS::Application::Trigger::Event->new($event_id);
565 if ($e->validate->valid) {
566 $logger->info("trigger: Event is valid, reacting...");
570 $e->editor->disconnect;
571 OpenILS::Application::Trigger::Event->ClearObjectCache();
575 reacted => $e->reacted,
576 cleanedup => $e->cleanedup,
580 __PACKAGE__->register_method(
581 api_name => 'open-ils.trigger.event.fire',
582 method => 'fire_single_event',
587 sub fire_event_group {
592 my $e = OpenILS::Application::Trigger::EventGroup->new(@$events);
594 if ($e->validate->valid) {
595 $logger->info("trigger: Event group is valid, reacting...");
599 $e->editor->disconnect;
600 OpenILS::Application::Trigger::Event->ClearObjectCache();
604 reacted => $e->reacted,
605 cleanedup => $e->cleanedup,
606 events => [map { $_->event } @{$e->events}]
609 __PACKAGE__->register_method(
610 api_name => 'open-ils.trigger.event_group.fire',
611 method => 'fire_event_group',
616 sub revalidate_event_group_test {
621 my $e = OpenILS::Application::Trigger::EventGroup->new_nochanges(@$events);
623 my $result = $e->revalidate_test;
625 $e->editor->disconnect;
626 OpenILS::Application::Trigger::Event->ClearObjectCache();
630 __PACKAGE__->register_method(
631 api_name => 'open-ils.trigger.event_group.revalidate.test',
632 method => 'revalidate_event_group_test',
636 desc => q/revalidate a group of events.
637 This does not actually update the events (so there will be no change
638 of atev.state or anything else in the database, unless an event's
639 validator makes changes out-of-band).
641 This returns an array of valid event IDs.
644 {name => "events", type => "array", desc => "list of event ids"}
653 my $granularity = shift;
654 my $granflag = shift;
656 my $query = [{ state => 'pending', run_time => {'<' => 'now'} }, { order_by => { atev => [ qw/run_time add_time/] }, 'join' => 'atevdef' }];
658 if (defined $granularity) {
660 $query->[0]->{'+atevdef'} = {granularity => $granularity};
662 $query->[0]->{'+atevdef'} = {'-or' => [ {granularity => $granularity}, {granularity => undef} ] };
665 $query->[0]->{'+atevdef'} = {granularity => undef};
668 return new_editor(xact=>1)->search_action_trigger_event(
669 $query, { idlist=> 1, timeout => 7200, substream => 1 }
672 __PACKAGE__->register_method(
673 api_name => 'open-ils.trigger.event.find_pending',
674 method => 'pending_events',
683 $e_ids = [$e_ids] if (!ref($e_ids));
686 for my $e_id (@$e_ids) {
689 $e = OpenILS::Application::Trigger::Event->new($e_id);
691 $logger->error("trigger: Event creation failed with ".shift());
694 next if !$e or $e->event->state eq 'invalid';
697 $e->build_environment;
699 $logger->error("trigger: Event environment building failed with ".shift());
702 $e->editor->disconnect;
703 $e->environment->{EventProcessor} = undef; # remove circular ref for json encoding
704 $client->respond($e);
707 OpenILS::Application::Trigger::Event->ClearObjectCache();
711 __PACKAGE__->register_method(
712 api_name => 'open-ils.trigger.event.gather',
713 method => 'gather_events',
720 my $granularity = shift;
721 my $granflag = shift;
723 my ($events) = $self->method_lookup('open-ils.trigger.event.find_pending')->run($granularity, $granflag);
725 my %groups = ( '*' => [] );
728 $logger->info("trigger: grouped_events found ".scalar(@$events)." pending events to process");
730 $logger->warn("trigger: grouped_events timed out loading pending events");
736 if ($parallel_collect == 1 or @$events == 1) { # use method lookup
737 @fleshed_events = $self->method_lookup('open-ils.trigger.event.gather')->run($events);
739 my $self_multi = OpenSRF::MultiSession->new(
740 app => 'open-ils.trigger',
741 cap => $parallel_collect,
742 success_handler => sub {
746 push @fleshed_events,
747 map { OpenILS::Application::Trigger::Event->new($_) }
749 @{ $req->{response} };
753 $self_multi->request( 'open-ils.trigger.event.gather' => $_ ) for ( @$events );
754 $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
756 $self_multi->session_wait(1);
757 $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
760 my @invalid; # sync for events with a null grouping field
761 for my $e (@fleshed_events) {
762 if (my $group = $e->event->event_def->group_field) {
764 # split the grouping link steps
765 my @steps = split /\./, $group;
766 my $group_field = pop(@steps); # we didn't flesh to this, it's a field not an object
771 $node = $node->$_() for ( @steps );
774 unless($node) { # should not get here, but to be safe..
779 # get the grouping value for the grouping object on this event
780 my $ident_value = $node->$group_field();
782 # could by false-y, so check definedness
783 if (!defined($ident_value)) {
788 if(ref $ident_value) {
789 my $ident_field = $ident_value->Identity;
790 $ident_value = $ident_value->$ident_field()
793 # push this event onto the event+grouping_value stack
794 $groups{$e->event->event_def->id}{$ident_value} ||= [];
795 push @{ $groups{$e->event->event_def->id}{$ident_value} }, $e;
797 # it's a non-grouped event
798 push @{ $groups{'*'} }, $e;
802 OpenILS::Application::Trigger::Event->invalidate(@invalid) if @invalid;
806 __PACKAGE__->register_method(
807 api_name => 'open-ils.trigger.event.find_pending_by_group',
808 method => 'grouped_events',
815 my $granularity = shift;
816 my $granflag = shift;
818 my ($groups) = $self->method_lookup('open-ils.trigger.event.find_pending_by_group')->run($granularity, $granflag);
819 $client->respond({"status" => "found"}) if (keys(%$groups) > 1 || @{$$groups{'*'}});
822 if ($parallel_react > 1 and (keys(%$groups) > 1 || @{$$groups{'*'}} > 1)) {
823 $self_multi = OpenSRF::MultiSession->new(
824 app => 'open-ils.trigger',
825 cap => $parallel_react,
826 session_hash_function => sub {
828 return $args->{target_id};
830 success_handler => sub {
833 $client->respond( $req->{response}->[0]->content );
838 for my $def ( keys %$groups ) {
840 $logger->info("trigger: run_all_events firing un-grouped events");
841 for my $event ( @{ $$groups{'*'} } ) {
844 $event->environment->{EventProcessor} = undef; # remove circular ref for json encoding
845 $self_multi->request({target_id => $event->id}, 'open-ils.trigger.event.fire', $event);
849 ->method_lookup('open-ils.trigger.event.fire')
854 $logger->error("trigger: event firing failed with ".shift());
857 $logger->info("trigger: run_all_events completed queuing un-grouped events");
858 $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
861 my $defgroup = $$groups{$def};
862 $logger->info("trigger: run_all_events firing events for grouped event def=$def");
863 for my $ident ( keys %$defgroup ) {
864 $logger->info("trigger: run_all_events firing group for grouped event def=$def and grp ident $ident");
867 $_->environment->{EventProcessor} = undef for @{$$defgroup{$ident}}; # remove circular ref for json encoding
868 $self_multi->request({target_id => $ident}, 'open-ils.trigger.event_group.fire', $$defgroup{$ident});
872 ->method_lookup('open-ils.trigger.event_group.fire')
873 ->run($$defgroup{$ident})
876 $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
878 $logger->error("trigger: event firing failed with ".shift());
881 $logger->info("trigger: run_all_events completed queuing events for grouped event def=$def");
885 $self_multi->session_wait(1) if ($self_multi);
886 $logger->info("trigger: run_all_events completed firing events");
888 $client->respond_complete();
891 __PACKAGE__->register_method(
892 api_name => 'open-ils.trigger.event.run_all_pending',
893 method => 'run_all_events',