package Time::Progress::Stored;
$Time::Progress::Stored::VERSION = '1.001';
use v5.10;
use Moo;
use true;
use autobox::Core;

=head1 NAME

Time::Progress::Stored - Report progress + store and retrieve the current status


This module helps if you have a long running process which reports
progress, but you need to actually display the progress to the user in
a different process.

Typically this is a long running web request in the web server or in a
job queue worker, while the web browser periodically sends Ajax
requests to get updated on the current progress status to show using a
progress bar.

Time::Progress::Stored stores the progress report as the worker
performs its job, and retrieves it from elsewhere where the report is

The report is a hashref with the following details:

    | id                | "progress-1454518121172"   |
    | max               | 262                        |
    | current           | 2                          |
    | activity          | "Importing Users"          |
    | elapsed_seconds   | "1"                        |
    | elapsed_time      | " 0:01"                    |
    | finish_time       | "Wed Feb 3 16:50:54 2016"  |
    | percent           | 0.8                        |
    | percent_string    | " 0.8%"                    |
    | remaining_seconds | "130"                      |
    | remaining_time    | " 2:10"                    |
    | is_done           | 0                          |


    ### In the worker process

    my $progess_id = ...; # Probably comes from the client
    my $max = @items * 2; # Let's say two passes over @items

    my $redis = Redis->new( ... );
    my $progress = Time::Progress::Stored->new({
        max         => $max,
        progress_id => $progress_id,
        storage     => Progress::Stored::Storage::Redis->new({ redis => $redis }),

    # Do the work
    for my $item (@items) {
        # Do stuff
        $progress->advance("Doing the first thing");
    for my $item (@items) {
        # Do other stuff
        $progress->advance("Doing other stuff with $item->{name}");


    ### HTTP endpoint, e.g. a Mojo controller for /progress/:progress_id

    sub get {
        my $self = shift;
        my $progress_id = $self->param("progress_id");

        my $storage = Time::Progress::Stored::Storage::Redis->new({
            redis => $self->redis,
        my $response_json = $storage->retrieve($progress_id)
            or return $self->render(status => 404, json => {});

        return $self->render(json => { progress => $response_json });

=head2 Synopsis breakdown

=head3 Worker

As the long running piece of work comes in, start the progress

Create the $progress object with a storage to store the progress
reports. Initialize it with the number of actions you want to perform.

For each time something is done, call $progress->advance(). This might
update the actual report.

By default only about 100 steps will be reported (each percent
progressed). You can increase this by specifying the attribute
"report_every", e.g. to report every single time something happens,
set it to 1 (this might slow things down though).

"advance" can optionally be called with an activity description
string, indicating what's happening right now. This can be large scale
steps, or include details about this specific action being performed.

At the end, call $progress->done, just to make sure the reporting
knows it has reached the end. This will set is_done: 1 in the report.

=head3 HTTP endpoint

The client should send requests to the endpoint periodically to get
the current progress report. The request contains the progress id, and
it's simply fetched and returned as a JSON payload.

See above for the contents of the payload.

=head3 Client making a request to a long running web request

This is for when the long running process is running as one web

A web page would typically provide a progress id while making the
initial Ajax request to kick off the long running process.

The client then sends Ajax requests to the HTTP endpoint (using the
id) every second to get updates on the progress. It does this until
the long running progress has responded.

If you look at the report payload you'll see there is a variety of
information to provide user feedback, e.g. percent done, time left,
what's going on etc.

=head3 Client making a request to a asynchronous worker process

This is for when the long running process is running as a background
process, e.g. in a job queue.

A web page would typically kick off the long running process. Instead
of the client passing in a progress id, the $progress object defaults
to a unique id (or you can just make one up yourself), and the HTTP
endpoint can report $progress->progress_id back to the client in the

The client then sends Ajax requests to the HTTP endpoint (using the
id) every second to get updates on the progress. It does this until
the progress report "is_done" is set to 1.

=head2 Storage backends

Time::Progress::Stored can use various backends to store the

Current backends are Redis and Memory. Memory is mainly for testing
and not very useful for real life scenarios.

The backends are tiny and it would be simple to write another backend
for e.g. a file, or using one of the Cache or CHI modules. Patches
more than welcome.

=head2 Pitfalls

If you're testing with a single threaded test server like Morbo, the
web browser's Ajax requests won't be processed until the long running
process has finished, so you'll never see the progress until it's all


use Types::Standard qw/ Int Bool Str /;
use List::AllUtils qw/ min /;
use Time::Progress;

use Time::Progress::Stored::Storage::Memory;


=head2 max

The number of times you're planning to call ->advance().


has max => (
    is       => "ro",
    isa      => Int,
    required => 1,

=head2 storage

A Storage object to store reports in.

Default: Time::Progress::Stored::Storage::Memory, but that's probably
only useful for testing.


has storage => (
    is  => "lazy",
    isa => sub { shift->isa("Time::Progress::Stored::Storage") },
sub _build_storage {
    my $self = shift;

=head2 progress_id

The id to identify this progress report with. Either set by the
client, or defaulted to a unique URL-param safe value you can return
to the client.


has progress_id => ( is => "lazy" );
sub _build_progress_id { "progress-" . time() . "-" . int(rand(10_000)) }

=head2 report_every

Number of calls to ->advance() for every time a new progress report is
generated and stored.

Default: a value that results in a report for every percent of


has report_every => ( is => "lazy" );
sub _build_report_every {
    my $self = shift;
    int($self->max / 100) || 1;

=head2 current

The current iteration. You could in theory set this yourself, but it's
recommended to call ->advance().


has current => (
    is      => "rw",
    isa     => Int,
    lazy    => 1,
    builder => "_build_current",
sub _build_current { 0 }
sub inc_current {
    my $self = shift;
    $self->current( $self->current + 1 );

has is_done => (
    is      => "rw",
    isa     => Bool,
    lazy    => 1,
    builder => "_build_is_done",
sub _build_is_done { 0 }

=head2 current_activity

Same as ->current, but for the current activity.


has current_activity => (
    is      => "rw",
    isa     => Str,
    lazy    => 1,
    builder => "_build_current_activity",
sub _build_current_activity { "" }

has progress => ( is => "lazy" );
sub _build_progress {
    my $self = shift;
    Time::Progress->new( max => $self->max // 1 )

=head1 METHODS


sub BUILD {
    my $self = shift;
    # There should always be a report available to fetch

=head2 advance($activity?)

Advance the progress one iteration. Optionally set the current


sub advance {
    my $self = shift;
    my ($activity) = @_;
    defined($activity) and $self->current_activity( $activity );
    $self->current % $self->report_every or $self->write_report();

=head2 done()

Mark the progress as being completed, and set the is_done key in the


sub done {
    my $self = shift;
    $self->current( $self->max );

sub write_report {
    my $self = shift;
    $self->storage->store( $self->progress_id, $self->report );

sub report {
    my $self = shift;

    # Avoid going beyond max, since that results in a "n/a"
    my $current = min($self->current, $self->max);

    state $progress_template = join("\t", qw/ %p %l %L %e %E %f /);
    my $report_string = $self->progress->report( $progress_template, $current );
    my (
        $percent_string, $elapsed_seconds, $elapsed_time,
        $remaining_seconds, $remaining_time, $finish_time,
    ) = split(/\t/, $report_string);
    my $percent = $self->percent_from_string($percent_string);

    return {
        id                => $self->progress_id,
        activity          => $self->current_activity,
        max               => $self->max,
        current           => $current,
        percent_string    => $percent_string,
        percent           => $percent,
        elapsed_seconds   => $elapsed_seconds,
        elapsed_time      => $elapsed_time,
        remaining_seconds => $remaining_seconds,
        remaining_time    => $remaining_time,
        finish_time       => $finish_time,
        is_done           => $self->is_done,

sub percent_from_string {
    my $self = shift;
    my ($percent_string) = @_;
    $percent_string =~ /([\d.]+)/ or return 0;
    return $1 + 0;


=head2 Author

Johan Lindstrom, C<< <johanl [AT]> >>

=head2 Contributors

=head2 Source code


=head2 Bug reports

Please report any bugs or feature requests on GitHub:


=head2 Caveats


Copyright 2016- Broadbean Technologies, All Rights Reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.


Thanks to Broadbean for providing time to open source this during one
of the regular Hack-days.

This module uses the excellent L<Time::Progress> module, which you
should be using if you just need a progress bar in a command line