use strict;
use 5.010;
use utf8;
use Moose;
our $VERSION = '0.03';
has '+jsonp' => (default => '_cb');
use Time::HiRes qw(time);
sub service_name {
return 'async-microservice-time';
}
sub get_routes {
return (
'datetime' => {
defaults => {
GET => 'GET_datetime',
POST => 'POST_datetime',
},
},
'datetime/:time_zone' => {
defaults => {
GET => 'GET_datetime',
POST => 'POST_datetime',
},
},
'datetime/:time_zone_part1/:time_zone_part2' => {
defaults => {GET => 'GET_datetime_capture',},
validations => {
time_zone_part1 => qr{^\w+$},
time_zone_part2 => qr{^\w+$},
},
},
'datetime/span/:s_date' => {defaults => {GET => 'GET_datetime_span'}},
'epoch' => {defaults => {GET => 'GET_epoch'}},
'sleep' => {defaults => {GET => 'GET_sleep'}},
);
}
sub GET_datetime {
my ( $self, $this_req ) = @_;
my $time_zone = $this_req->params->{time_zone} // 'UTC';
return $self->_get_datetime_time_zone( $this_req, $time_zone );
}
sub GET_datetime_capture {
my ( $self, $this_req, $match ) = @_;
my $time_zone = $this_req->params->{time_zone_part1} . '/'
. $this_req->params->{time_zone_part2};
return $self->_get_datetime_time_zone( $this_req, $time_zone );
}
sub _get_datetime_time_zone {
my ( $self, $this_req, $time_zone ) = @_;
my $time_dt = eval { DateTime->now( time_zone => $time_zone ); };
if ($@) {
return [
405,
[],
{ err_status => 405,
err_msg => $@,
}
];
}
return [ 200, [], _datetime_as_data($time_dt) ];
}
sub POST_datetime {
my ($self, $this_req) = @_;
my $epoch = eval {$this_req->json_content->{epoch}};
if (!defined($epoch)) {
return [
405,
[],
{ err_status => 405,
err_msg => $@ || 'epoch data missing',
}
];
}
if ($epoch !~ m/^-?[0-9]+$/) {
return [
405,
[],
{ err_status => 405,
err_msg => 'epoch not a number',
}
];
}
return [200, [], _datetime_as_data(DateTime->from_epoch(epoch => $epoch))];
}
sub GET_epoch {
my ( $self, $this_req ) = @_;
return [ 200, [], { epoch => time() } ];
}
async sub GET_sleep {
my ( $self, $this_req ) = @_;
my $start_time = time();
my $sleep_time = ( $this_req->params->{duration} // rand(10) ) + 0;
if ( $sleep_time <= 0 ) {
return [
405,
[],
{ err_status => 405,
err_msg => 'invalid sleep duration',
}
];
}
await AnyEvent::Future->new_delay( after => $sleep_time );
my $stop_time = time();
return [
200,
[],
{ start => $start_time,
stop => $stop_time,
duration => ( $stop_time - $start_time ),
}
];
}
async sub GET_datetime_span {
my ( $self, $this_req ) = @_;
my $now = DateTime->now(time_zone => 'UTC')->truncate(to => 'day');
my $s_date_dt;
my $s_date = $this_req->params->{s_date} // '';
if ($s_date =~ m/^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/) {
$s_date_dt = eval {DateTime->new(time_zone => 'UTC', day => $3, month => $2, year => $1)};
} elsif ($s_date eq 'now') {
$s_date_dt = $now->clone;
}
unless ($s_date_dt) {
return [
405,
[],
{ err_status => 405,
err_msg => 'invalid date format ' . $s_date . ', use YYYYMMDD or "now"',
}
];
}
my $r_age = int($this_req->params->{r_age} // 65);
unless ($r_age || ($r_age < 1) || ($r_age > 200)) {
return [
405,
[],
{ err_status => 405,
err_msg => 'invalid age',
}
];
}
my $m_income = int($this_req->params->{m_income} // 0);
my $r_age_dt = $s_date_dt->clone->add(years => $r_age);
$r_age_dt = $now->clone
if $r_age_dt < $now;
my $counter_dt = $r_age_dt->subtract_datetime($now);
my $days_counter = $r_age_dt->delta_days($now)->in_units('days');
my $week_counter = int($days_counter/7);
my $months_counter = $counter_dt->in_units('months');
my $years_counter = $counter_dt->in_units('years');
my $date_from_str = $s_date_dt->strftime('%Y-%m-%d');
my $date_to_str = $r_age_dt->strftime('%Y-%m-%d');
return {
msg => sprintf(
'%d weeks left until date span age of %d (date from %s to %s)',
$week_counter, $r_age, $date_from_str, $date_to_str,
),
days => $days_counter,
weeks => $week_counter,
months => $months_counter,
years => $years_counter,
date_from => $date_from_str,
date_to => $date_to_str,
($m_income ? (income => $months_counter * $m_income) : ()),
};
}
sub _datetime_as_data {
my ($dt) = @_;
return {
datetime => $dt->strftime('%Y-%m-%d %H:%M:%S %z'),
date => $dt->strftime('%Y-%m-%d'),
time => $dt->strftime('%H:%M:%S'),
time_zone => $dt->strftime('%z'),
time_zone_name => $dt->strftime('%Z'),
day => $dt->strftime('%d'),
month => $dt->strftime('%m'),
year => $dt->strftime('%Y'),
hour => $dt->strftime('%H'),
minute => $dt->strftime('%M'),
second => $dt->strftime('%S'),
epoch => $dt->epoch,
};
}
__PACKAGE__->meta->make_immutable;
1;
__END__
=head1 NAME
Async::Microservice::Time - example time async microservice
=head1 SYNOPSYS
# can be started using:
plackup --port 8085 -Ilib --access-log /dev/null --server Twiggy bin/async-microservice-time.psgi
curl "http://localhost:8085/v1/hcheck" -H "accept: application/json"
curl "http://localhost:8085/v1/epoch" -H "accept: application/json"
curl "http://localhost:8085/v1/datetime?time_zone=local" -H "accept: application/json"
=head1 DESCRIPTION
This is an example asynchronous http micro service using L<Async::Microservice>.
View the source code it's minimal.
=head1 METHODS
=head2 service_name
Just a name, used to identify process and look for OpenAPI documentation.
=head2 get_routes
L<Path::Router> configuration for dispatching
=head2 http response methods
=head3 GET_datetime
=head3 POST_datetime
$ curl -X POST "https://time.meon.eu/v1/datetime" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"epoch\":-42}"
{
"date" : "1969-12-31",
"datetime" : "1969-12-31 23:59:18 +0000",
"day" : "31",
"epoch" : -42,
"hour" : "23",
"minute" : "59",
"month" : "12",
"second" : "18",
"time" : "23:59:18",
"time_zone" : "+0000",
"time_zone_name" : "UTC",
"year" : "1969"
}
=head3 GET_epoch
=head3 GET_sleep
This is the only parallel processed reponse method (the other ones are
pure CPU-only bound) that sleep given (or random) number of seconds and
only then returns the request response with when it started and how long
it took. Normally this the same as what is in duration parameter, but in
case the server is overloaded with requests, the event loop may call the
timer handler much later than the duration. Try:
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 259 432.8 21 1033
Processing: 3001 3090 72.5 3061 3253
Waiting: 3001 3090 72.5 3061 3253
Total: 3022 3349 394.1 3155 4065
Then try to run together with 100% CPU load:
ab -q -n 10000 -c 50 http://localhost:8085/v1/datetime
=head3 the rest
Check out L<Async::Microservice> for built-in http response methods.
=head1 SEE ALSO
F<t/02_Async-Microservice-Time.t> for an example how to test this service.
=cut