convert checkout to promises (checkout_p)

This commit is contained in:
Birte Kristina Friesel 2023-07-23 20:18:10 +02:00
parent c0754f9e87
commit 38ad42b42c
No known key found for this signature in database
GPG key ID: 19E6E524EBB177BA
3 changed files with 347 additions and 256 deletions

View file

@ -588,7 +588,7 @@ sub startup {
); );
$self->helper( $self->helper(
'checkout' => sub { 'checkout_p' => sub {
my ( $self, %opt ) = @_; my ( $self, %opt ) = @_;
my $station = $opt{station}; my $station = $opt{station};
@ -596,34 +596,29 @@ sub startup {
my $arr_eva = $opt{arr_eva}; my $arr_eva = $opt{arr_eva};
my $with_related = $opt{with_related} // 0; my $with_related = $opt{with_related} // 0;
my $force = $opt{force}; my $force = $opt{force};
my $uid = $opt{uid}; my $uid = $opt{uid} // $self->current_user->{id};
my $db = $opt{db} // $self->pg->db; my $db = $opt{db} // $self->pg->db;
my $status = $self->iris->get_departures( my $user = $self->get_user_status( $uid, $db );
station => $station, my $train_id = $user->{train_id};
lookbehind => 120,
lookahead => 180, my $promise = Mojo::Promise->new;
with_related => $with_related,
);
$uid //= $self->current_user->{id};
my $user = $self->get_user_status( $uid, $db );
my $train_id = $user->{train_id};
if ( not $station ) { if ( not $station ) {
$self->app->log->error("Checkout($uid): station is empty"); $self->app->log->error("Checkout($uid): station is empty");
return ( 1, 'BUG: Checkout station is empty.' ); return $promise->resolve( 1,
'BUG: Checkout station is empty.' );
} }
if ( not $user->{checked_in} and not $user->{cancelled} ) { if ( not $user->{checked_in} and not $user->{cancelled} ) {
return ( 0, 'You are not checked into any train' ); return $promise->resolve( 0,
} 'You are not checked into any train' );
if ( $status->{errstr} and not $force ) {
return ( 1, $status->{errstr} );
} }
if ( $dep_eva and $dep_eva != $user->{dep_eva} ) { if ( $dep_eva and $dep_eva != $user->{dep_eva} ) {
return ( 0, 'race condition' ); return $promise->resolve( 0, 'race condition' );
} }
if ( $arr_eva and $arr_eva != $user->{arr_eva} ) { if ( $arr_eva and $arr_eva != $user->{arr_eva} ) {
return ( 0, 'race condition' ); return $promise->resolve( 0, 'race condition' );
} }
my $now = DateTime->now( time_zone => 'Europe/Berlin' ); my $now = DateTime->now( time_zone => 'Europe/Berlin' );
@ -632,196 +627,232 @@ sub startup {
with_data => 1 with_data => 1
); );
# Note that a train may pass the same station several times. $self->iris->get_departures_p(
# Notable example: S41 / S42 ("Ringbahn") both starts and station => $station,
# terminates at Berlin Südkreuz lookbehind => 120,
my ($train) = List::Util::first { lookahead => 180,
$_->train_id eq $train_id with_related => $with_related,
and $_->sched_arrival )->then(
and $_->sched_arrival->epoch > $user->{sched_departure}->epoch sub {
} my ($status) = @_;
@{ $status->{results} };
$train //= List::Util::first { $_->train_id eq $train_id } my $new_checkout_station_id = $status->{station_eva};
@{ $status->{results} };
my $new_checkout_station_id = $status->{station_eva}; # Store the intended checkout station regardless of this operation's
# success.
# TODO for with_related == 1, the correct EVA may be different
# and should be fetched from $train later on
$self->in_transit->set_arrival_eva(
uid => $uid,
db => $db,
arrival_eva => $new_checkout_station_id
);
# Store the intended checkout station regardless of this operation's # If in_transit already contains arrival data for another estimated
# success. # destination, we must invalidate it.
$self->in_transit->set_arrival_eva( if ( defined $journey->{checkout_station_id}
uid => $uid, and $journey->{checkout_station_id}
db => $db, != $new_checkout_station_id )
arrival_eva => $new_checkout_station_id {
); $self->in_transit->unset_arrival_data(
uid => $uid,
db => $db
);
}
# If in_transit already contains arrival data for another estimated # Note that a train may pass the same station several times.
# destination, we must invalidate it. # Notable example: S41 / S42 ("Ringbahn") both starts and
if ( defined $journey->{checkout_station_id} # terminates at Berlin Südkreuz
and $journey->{checkout_station_id} my $train = List::Util::first {
!= $new_checkout_station_id ) $_->train_id eq $train_id
{ and $_->sched_arrival
$self->in_transit->unset_arrival_data( and $_->sched_arrival->epoch
uid => $uid, > $user->{sched_departure}->epoch
db => $db }
); @{ $status->{results} };
}
if ( not defined $train ) { $train //= List::Util::first { $_->train_id eq $train_id }
@{ $status->{results} };
# Arrival time via IRIS is unknown, so the train probably has not if ( not defined $train ) {
# arrived yet. Fall back to HAFAS.
# TODO support cases where $station is EVA or DS100 code # Arrival time via IRIS is unknown, so the train probably
if ( # has not arrived yet. Fall back to HAFAS.
my $station_data # TODO support cases where $station is EVA or DS100 code
= List::Util::first { $_->[0] eq $station } if (
@{ $journey->{route} } my $station_data
) = List::Util::first { $_->[0] eq $station }
{ @{ $journey->{route} }
$station_data = $station_data->[2]; )
if ( $station_data->{sched_arr} ) { {
my $sched_arr $station_data = $station_data->[2];
= epoch_to_dt( $station_data->{sched_arr} ); if ( $station_data->{sched_arr} ) {
my $rt_arr = epoch_to_dt( $station_data->{rt_arr} ); my $sched_arr
if ( $rt_arr->epoch == 0 ) { = epoch_to_dt( $station_data->{sched_arr} );
$rt_arr = $sched_arr->clone; my $rt_arr
if ( $station_data->{arr_delay} = epoch_to_dt( $station_data->{rt_arr} );
and $station_data->{arr_delay} =~ m{^\d+$} ) if ( $rt_arr->epoch == 0 ) {
{ $rt_arr = $sched_arr->clone;
$rt_arr->add( if ( $station_data->{arr_delay}
minutes => $station_data->{arr_delay} ); and $station_data->{arr_delay}
=~ m{^\d+$} )
{
$rt_arr->add( minutes =>
$station_data->{arr_delay} );
}
}
$self->in_transit->set_arrival_times(
uid => $uid,
db => $db,
sched_arrival => $sched_arr,
rt_arrival => $rt_arr
);
} }
} }
$self->in_transit->set_arrival_times( if ( not $force ) {
uid => $uid,
db => $db,
sched_arrival => $sched_arr,
rt_arrival => $rt_arr
);
}
}
if ( not $force ) {
# mustn't be called during a transaction # mustn't be called during a transaction
if ( not $opt{in_transaction} ) { if ( not $opt{in_transaction} ) {
$self->run_hook( $uid, 'update' ); $self->run_hook( $uid, 'update' );
} }
return ( 1, undef ); $promise->resolve( 1, undef );
} return;
}
my $has_arrived = 0;
eval {
my $tx;
if ( not $opt{in_transaction} ) {
$tx = $db->begin;
}
if ( defined $train and not $train->arrival and not $force ) {
my $train_no = $train->train_no;
die("Train ${train_no} has no arrival timestamp\n");
}
elsif ( defined $train and $train->arrival ) {
$self->in_transit->set_arrival(
uid => $uid,
db => $db,
train => $train,
route => [ $self->iris->route_diff($train) ]
);
$has_arrived = $train->arrival->epoch < $now->epoch ? 1 : 0;
if ($has_arrived) {
my @unknown_stations
= $self->stations->grep_unknown( $train->route );
if (@unknown_stations) {
$self->app->log->warn(
sprintf(
'Route of %s %s (%s -> %s) contains unknown stations: %s',
$train->type,
$train->train_no,
$train->origin,
$train->destination,
join( ', ', @unknown_stations )
)
);
} }
} }
} my $has_arrived = 0;
$journey = $self->in_transit->get( eval {
uid => $uid,
db => $db
);
if ( $has_arrived or $force ) { my $tx;
$self->journeys->add_from_in_transit( if ( not $opt{in_transaction} ) {
db => $db, $tx = $db->begin;
journey => $journey }
);
$self->in_transit->delete(
uid => $uid,
db => $db
);
my $cache_ts = $now->clone; if ( defined $train
if ( $journey->{real_departure} and not $train->arrival
=~ m{ ^ (?<year> \d{4} ) - (?<month> \d{2} ) }x ) and not $force )
{ {
$cache_ts->set( my $train_no = $train->train_no;
year => $+{year}, die("Train ${train_no} has no arrival timestamp\n");
month => $+{month} }
elsif ( defined $train and $train->arrival ) {
$self->in_transit->set_arrival(
uid => $uid,
db => $db,
train => $train,
route => [ $self->iris->route_diff($train) ]
);
$has_arrived
= $train->arrival->epoch < $now->epoch ? 1 : 0;
if ($has_arrived) {
my @unknown_stations
= $self->stations->grep_unknown(
$train->route );
if (@unknown_stations) {
$self->app->log->warn(
sprintf(
'Route of %s %s (%s -> %s) contains unknown stations: %s',
$train->type,
$train->train_no,
$train->origin,
$train->destination,
join( ', ', @unknown_stations )
)
);
}
}
}
$journey = $self->in_transit->get(
uid => $uid,
db => $db
); );
if ( $has_arrived or $force ) {
$self->journeys->add_from_in_transit(
db => $db,
journey => $journey
);
$self->in_transit->delete(
uid => $uid,
db => $db
);
my $cache_ts = $now->clone;
if ( $journey->{real_departure}
=~ m{ ^ (?<year> \d{4} ) - (?<month> \d{2} ) }x
)
{
$cache_ts->set(
year => $+{year},
month => $+{month}
);
}
$self->journey_stats_cache->invalidate(
ts => $cache_ts,
db => $db,
uid => $uid
);
}
elsif ( defined $train
and $train->arrival_is_cancelled )
{
# This branch is only taken if the deparure was not cancelled,
# i.e., if the train was supposed to go here but got
# redirected or cancelled on the way and not from the start on.
# If the departure itself was cancelled, the user route is
# cancelled_from action -> 'cancelled journey' panel on main page
# -> cancelled_to action -> force checkout (causing the
# previous branch to be taken due to $force)
$journey->{cancelled} = 1;
$self->journeys->add_from_in_transit(
db => $db,
journey => $journey
);
$self->in_transit->set_cancelled_destination(
uid => $uid,
db => $db,
cancelled_destination => $train->station,
);
}
if ( not $opt{in_transaction} ) {
$tx->commit;
}
};
if ($@) {
$self->app->log->error("Checkout($uid): $@");
$promise->resolve( 1, 'Checkout error: ' . $@ );
return;
} }
$self->journey_stats_cache->invalidate(
ts => $cache_ts,
db => $db,
uid => $uid
);
}
elsif ( defined $train and $train->arrival_is_cancelled ) {
# This branch is only taken if the deparure was not cancelled, if ( $has_arrived or $force ) {
# i.e., if the train was supposed to go here but got if ( not $opt{in_transaction} ) {
# redirected or cancelled on the way and not from the start on. $self->run_hook( $uid, 'checkout' );
# If the departure itself was cancelled, the user route is }
# cancelled_from action -> 'cancelled journey' panel on main page $promise->resolve( 0, undef );
# -> cancelled_to action -> force checkout (causing the return;
# previous branch to be taken due to $force) }
$journey->{cancelled} = 1; if ( not $opt{in_transaction} ) {
$self->journeys->add_from_in_transit( $self->run_hook( $uid, 'update' );
db => $db, $self->add_route_timestamps( $uid, $train, 0, 1 );
journey => $journey }
); $promise->resolve( 1, undef );
$self->in_transit->set_cancelled_destination( return;
uid => $uid,
db => $db,
cancelled_destination => $train->station,
);
}
if ( not $opt{in_transaction} ) {
$tx->commit;
} }
}; )->catch(
sub {
if ($@) { my ($err) = @_;
$self->app->log->error("Checkout($uid): $@"); $promise->resolve( 1, $err );
return ( 1, 'Checkout error: ' . $@ ); return;
}
if ( $has_arrived or $force ) {
if ( not $opt{in_transaction} ) {
$self->run_hook( $uid, 'checkout' );
} }
return ( 0, undef ); )->wait;
}
if ( not $opt{in_transaction} ) { return $promise;
$self->run_hook( $uid, 'update' );
$self->add_route_timestamps( $uid, $train, 0, 1 );
}
return ( 1, undef );
} }
); );
@ -1788,13 +1819,17 @@ sub startup {
)->then( )->then(
sub { sub {
$self->log->debug("... handled origin"); $self->log->debug("... handled origin");
my ( undef, $err ) = $self->checkout( return $self->checkout_p(
station => $traewelling->{arr_eva}, station => $traewelling->{arr_eva},
train_id => 0, train_id => 0,
uid => $uid, uid => $uid,
in_transaction => 1, in_transaction => 1,
db => $db db => $db
); );
}
)->then(
sub {
my ( undef, $err ) = @_;
if ($err) { if ($err) {
$self->log->debug("... error: $err"); $self->log->debug("... error: $err");
return Mojo::Promise->reject($err); return Mojo::Promise->reject($err);

View file

@ -86,15 +86,15 @@ sub run {
# train. # train.
if ($checked_in) { if ($checked_in) {
# check out (adds a cancelled journey and resets journey state # check out (adds a cancelled journey and resets journey state
# to checkin # to checkin
$self->app->checkout( $self->app->checkout_p(
station => $arr, station => $arr,
force => 1, force => 2,
dep_eva => $dep, dep_eva => $dep,
arr_eva => $arr, arr_eva => $arr,
uid => $uid uid => $uid
); )->wait;
} }
} }
else { else {
@ -153,15 +153,15 @@ sub run {
if ( $checked_in and $train->arrival_is_cancelled ) { if ( $checked_in and $train->arrival_is_cancelled ) {
# check out (adds a cancelled journey and resets journey state # check out (adds a cancelled journey and resets journey state
# to destination selection) # to destination selection)
$self->app->checkout( $self->app->checkout_p(
station => $arr, station => $arr,
force => 0, force => 0,
dep_eva => $dep, dep_eva => $dep,
arr_eva => $arr, arr_eva => $arr,
uid => $uid uid => $uid
); )->wait;
} }
else { else {
$self->app->add_route_timestamps( $self->app->add_route_timestamps(
@ -174,21 +174,24 @@ sub run {
} }
} }
elsif ( $entry->{real_arr_ts} ) { elsif ( $entry->{real_arr_ts} ) {
my ( undef, $error ) = $self->app->checkout( my ( undef, $error ) = $self->app->checkout_p(
station => $arr, station => $arr,
force => 1, force => 2,
dep_eva => $dep, dep_eva => $dep,
arr_eva => $arr, arr_eva => $arr,
uid => $uid uid => $uid
); )->catch(
if ($error) { sub {
die("${error}\n"); my ($error) = @_;
} $self->app->log->error("work($uid)/arrival: $@");
$errors += 1;
}
)->wait;
} }
}; };
if ($@) { if ($@) {
$errors += 1;
$self->app->log->error("work($uid)/arrival: $@"); $self->app->log->error("work($uid)/arrival: $@");
$errors += 1;
} }
eval { } eval { }

View file

@ -622,17 +622,26 @@ sub travel_action {
if ( $params->{action} eq 'checkin' ) { if ( $params->{action} eq 'checkin' ) {
my $status = $self->get_user_status; my $status = $self->get_user_status;
my $promise;
if ( $status->{checked_in} if ( $status->{checked_in}
and $status->{arr_eva} and $status->{arr_eva}
and $status->{arrival_countdown} <= 0 ) and $status->{arrival_countdown} <= 0 )
{ {
$self->checkout( station => $status->{arr_eva} ); $promise = $self->checkout_p( station => $status->{arr_eva} );
}
else {
$promise = Mojo::Promise->resolve;
} }
$self->render_later; $self->render_later;
$self->checkin_p( $promise->then(
station => $params->{station}, sub {
train_id => $params->{train} return $self->checkin_p(
station => $params->{station},
train_id => $params->{train}
);
}
)->then( )->then(
sub { sub {
my $destination = $params->{dest}; my $destination = $params->{dest};
@ -648,17 +657,26 @@ sub travel_action {
# Silently ignore errors -- if they are permanent, the user will see # Silently ignore errors -- if they are permanent, the user will see
# them when selecting the destination manually. # them when selecting the destination manually.
my ( $still_checked_in, undef ) = $self->checkout( return $self->checkout_p(
station => $destination, station => $destination,
force => 0 force => 0
); );
my $station_link = '/s/' . $destination; }
$self->render( )->then(
json => { sub {
success => 1, my ( $still_checked_in, undef ) = @_;
redirect_to => $still_checked_in ? '/' : $station_link, if ( my $destination = $params->{dest} ) {
}, my $station_link = '/s/' . $destination;
); $self->render(
json => {
success => 1,
redirect_to => $still_checked_in
? '/'
: $station_link,
},
);
}
return;
} }
)->catch( )->catch(
sub { sub {
@ -673,28 +691,47 @@ sub travel_action {
)->wait; )->wait;
} }
elsif ( $params->{action} eq 'checkout' ) { elsif ( $params->{action} eq 'checkout' ) {
my ( $still_checked_in, $error ) = $self->checkout( $self->render_later;
$self->checkout_p(
station => $params->{station}, station => $params->{station},
force => $params->{force} force => $params->{force}
); )->then(
my $station_link = '/s/' . $params->{station}; sub {
my ( $still_checked_in, $error ) = @_;
my $station_link = '/s/' . $params->{station};
if ($error) { if ($error) {
$self->render( $self->render(
json => { json => {
success => 0, success => 0,
error => $error, error => $error,
}, },
); );
} }
else { else {
$self->render( $self->render(
json => { json => {
success => 1, success => 1,
redirect_to => $still_checked_in ? '/' : $station_link, redirect_to => $still_checked_in
}, ? '/'
); : $station_link,
} },
);
}
return;
}
)->catch(
sub {
my ($error) = @_;
$self->render(
json => {
success => 0,
error => $error,
},
);
return;
}
)->wait;
} }
elsif ( $params->{action} eq 'undo' ) { elsif ( $params->{action} eq 'undo' ) {
my $status = $self->get_user_status; my $status = $self->get_user_status;
@ -747,27 +784,43 @@ sub travel_action {
)->wait; )->wait;
} }
elsif ( $params->{action} eq 'cancelled_to' ) { elsif ( $params->{action} eq 'cancelled_to' ) {
my ( undef, $error ) = $self->checkout( $self->render_later;
$self->checkout_p(
station => $params->{station}, station => $params->{station},
force => 1 force => 1
); )->then(
sub {
if ($error) { my ( undef, $error ) = @_;
$self->render( if ($error) {
json => { $self->render(
success => 0, json => {
error => $error, success => 0,
}, error => $error,
); },
} );
else { }
$self->render( else {
json => { $self->render(
success => 1, json => {
redirect_to => '/', success => 1,
}, redirect_to => '/',
); },
} );
}
return;
}
)->catch(
sub {
my ($error) = @_;
$self->render(
json => {
success => 0,
error => $error,
},
);
return;
}
)->wait;
} }
elsif ( $params->{action} eq 'delete' ) { elsif ( $params->{action} eq 'delete' ) {
my $error = $self->journeys->delete( my $error = $self->journeys->delete(