Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
...
  • 5 commits
  • 15 files changed
  • 0 commit comments
  • 1 contributor
View
28 ddl/templates/versions/8/audit-up.sql
@@ -0,0 +1,28 @@
+CREATE TABLE audit.cached_reports (
+audit_op CHAR(1) NOT NULL CHECK (audit_op IN ('D','I','U')),
+audit_user TEXT NOT NULL,
+audit_stamp TIMESTAMP NOT NULL,
+audit_txid INTEGER NOT NULL,
+id character(36),
+report_class text,
+params text,
+expires timestamp without time zone
+);
+GRANT SELECT ON audit.cached_reports TO "[% ro_role %]";
+GRANT SELECT,INSERT ON audit.cached_reports TO "[% rw_role %]";
+CREATE OR REPLACE FUNCTION public.process_cached_reports_audit()
+RETURNS TRIGGER AS $cached_reports_audit$
+ BEGIN
+ IF (TG_OP = 'DELETE') THEN
+ INSERT INTO audit.cached_reports SELECT 'D', user, now(), txid_current(), OLD.*;
+ ELSIF (TG_OP = 'UPDATE') THEN
+ INSERT INTO audit.cached_reports SELECT 'U', user, now(), txid_current(), NEW.*;
+ ELSIF (TG_OP = 'INSERT') THEN
+ INSERT INTO audit.cached_reports SELECT 'I', user, now(), txid_current(), NEW.*;
+ END IF;
+ RETURN NULL;
+ END;
+$cached_reports_audit$ LANGUAGE plpgsql;
+CREATE TRIGGER cached_reports_audit
+AFTER INSERT OR UPDATE OR DELETE ON public.cached_reports
+ FOR EACH ROW EXECUTE PROCEDURE public.process_cached_reports_audit();
View
12 ddl/templates/versions/8/up.sql
@@ -0,0 +1,12 @@
+CREATE TABLE cached_reports (
+ id CHAR(36) PRIMARY KEY,
+ report_class TEXT NOT NULL,
+ params TEXT NOT NULL,
+ expires TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + INTERVAL '8 hours',
+ complete BOOLEAN NOT NULL DEFAULT FALSE
+);
+GRANT SELECT ON cached_reports TO "[% ro_role %]";
+GRANT SELECT, INSERT, UPDATE, DELETE ON cached_reports TO "[% rw_role %]";
+CREATE INDEX cached_reports_report_class_params_idx ON cached_reports(report_class,params);
+
+-- Intentionally no audit for cached_reports.
View
107 lib/LIMS2/Model/Schema/Result/CachedReport.pm
@@ -0,0 +1,107 @@
+use utf8;
+package LIMS2::Model::Schema::Result::CachedReport;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+=head1 NAME
+
+LIMS2::Model::Schema::Result::CachedReport
+
+=cut
+
+use strict;
+use warnings;
+
+use Moose;
+use MooseX::NonMoose;
+use MooseX::MarkAsMethods autoclean => 1;
+extends 'DBIx::Class::Core';
+
+=head1 COMPONENTS LOADED
+
+=over 4
+
+=item * L<DBIx::Class::InflateColumn::DateTime>
+
+=back
+
+=cut
+
+__PACKAGE__->load_components("InflateColumn::DateTime");
+
+=head1 TABLE: C<cached_reports>
+
+=cut
+
+__PACKAGE__->table("cached_reports");
+
+=head1 ACCESSORS
+
+=head2 id
+
+ data_type: 'char'
+ is_nullable: 0
+ size: 36
+
+=head2 report_class
+
+ data_type: 'text'
+ is_nullable: 0
+
+=head2 params
+
+ data_type: 'text'
+ is_nullable: 0
+
+=head2 expires
+
+ data_type: 'timestamp'
+ default_value: (now() + '08:00:00'::interval)
+ is_nullable: 0
+
+=head2 complete
+
+ data_type: 'boolean'
+ default_value: false
+ is_nullable: 0
+
+=cut
+
+__PACKAGE__->add_columns(
+ "id",
+ { data_type => "char", is_nullable => 0, size => 36 },
+ "report_class",
+ { data_type => "text", is_nullable => 0 },
+ "params",
+ { data_type => "text", is_nullable => 0 },
+ "expires",
+ {
+ data_type => "timestamp",
+ default_value => \"(now() + '08:00:00'::interval)",
+ is_nullable => 0,
+ },
+ "complete",
+ { data_type => "boolean", default_value => \"false", is_nullable => 0 },
+);
+
+=head1 PRIMARY KEY
+
+=over 4
+
+=item * L</id>
+
+=back
+
+=cut
+
+__PACKAGE__->set_primary_key("id");
+
+
+# Created by DBIx::Class::Schema::Loader v0.07022 @ 2012-07-31 16:36:06
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:++CtoB4pjLtlShaOGgBGdA
+
+
+# You can replace this text with custom code or comments, and it will be preserved on regeneration
+__PACKAGE__->meta->make_immutable;
+1;
View
68 lib/LIMS2/Report.pm
@@ -4,7 +4,7 @@ use strict;
use warnings FATAL => 'all';
use Sub::Exporter -setup => {
- exports => [ 'generator_for', 'generate_report' ]
+ exports => [ 'generator_for', 'generate_report', 'cached_report' ]
};
use Data::UUID;
@@ -13,9 +13,55 @@ use Text::CSV;
use IO::Pipe;
use Path::Class;
use Log::Log4perl qw( :easy );
+use Scalar::Util qw( blessed );
+use Fcntl qw( :DEFAULT :flock );
use LIMS2::Exception::System;
use LIMS2::Exception::Validation;
+sub _cached_report_ok {
+ my ( $work_dir, $cache_entry ) = @_;
+
+ my $report_dir = $work_dir->subdir( $cache_entry->id );
+ if ( $report_dir->stat && ! $report_dir->file('fail')->stat ) {
+ return 1;
+ }
+
+ $cache_entry->delete;
+ return;
+}
+
+sub cached_report {
+ my %args = @_;
+
+ my $generator = generator_for( $args{report}, $args{model}, $args{params} );
+
+ # Take an exclusive lock to avoid race between interrogating table
+ # and creating cache row. This ensures we don't set off concurrent
+ # cache refreshes for the same report.
+ my $lock_file = $args{output_dir}->file('lims2.cache.lock');
+ my $lock_fh = $lock_file->open( O_RDWR|O_CREAT, oct(644) )
+ or LIMS2::Exception::System->throw( "open $lock_file failed: $!" );
+ local $SIG{ALRM} = sub { LIMS2::Exception::System->throw( "Timeout waiting for lock on $lock_file" ) };
+ alarm(10);
+ flock( $lock_fh, LOCK_EX )
+ or LIMS2::Exception::System->throw( "flock $lock_file failed: $!" );
+ alarm(0);
+
+ if ( my $in_cache = $generator->cached_report ) {
+ if ( _cached_report_ok( $args{output_dir}, $in_cache ) ) {
+ return $in_cache->id;
+ }
+ }
+
+ my $cache_entry = $generator->init_cached_report( generate_report_id() );
+ $lock_fh->close(); # End of critical code: release the lock
+
+ my $work_dir = init_work_dir( $args{output_dir}, $cache_entry->id );
+ run_in_background( $generator, $work_dir, $cache_entry );
+
+ return $cache_entry->id;
+}
+
sub generator_for {
my ( $report, $model, $params ) = @_;
@@ -26,12 +72,16 @@ sub generator_for {
return $generator;
}
+sub generate_report_id {
+ return Data::UUID->new->create_str();
+}
+
sub generate_report {
my %args = @_;
my $generator = generator_for( $args{report}, $args{model}, $args{params} );
- my $report_id = Data::UUID->new->create_str();
+ my $report_id = generate_report_id();
INFO( "Generating $args{report} report $report_id" );
@@ -49,7 +99,7 @@ sub generate_report {
}
sub run_in_background {
- my ( $generator, $work_dir ) = @_;
+ my ( $generator, $work_dir, $cache_entry ) = @_;
local $SIG{CHLD} = 'IGNORE';
@@ -58,7 +108,9 @@ sub run_in_background {
if ( $pid == 0 ) { # child
Log::Log4perl->easy_init( { level => $WARN, file => $work_dir->file( 'log' ) } );
- do_generate_report( $generator, $work_dir );
+ $generator->model->clear_schema; # Force re-connect in child process
+ local $0 = 'Generate report ' . $generator->name;
+ do_generate_report( $generator, $work_dir, $cache_entry );
exit 0;
}
@@ -66,13 +118,13 @@ sub run_in_background {
}
sub run_in_foreground {
- my ( $generator, $work_dir ) = @_;
+ my ( $generator, $work_dir, $cache_entry ) = @_;
- return do_generate_report( $generator, $work_dir );
+ return do_generate_report( $generator, $work_dir, $cache_entry );
}
sub do_generate_report {
- my ( $generator, $work_dir ) = @_;
+ my ( $generator, $work_dir, $cache_entry ) = @_;
my $ok = 0;
@@ -95,11 +147,13 @@ sub do_generate_report {
write_report_name( $work_dir->file( 'name' ), $generator->name );
$work_dir->file( 'done' )->touch;
+ $cache_entry && $cache_entry->update( { complete => 1 } );
$ok = 1;
}
catch {
ERROR $_;
$work_dir->file( 'failed' )->touch;
+ $cache_entry && $cache_entry->delete;
};
return $ok;
View
4 lib/LIMS2/Report/ElectroporationProductionSummary.pm
@@ -17,6 +17,10 @@ has species => (
required => 1
);
+has '+param_names' => (
+ default => sub { [ 'species' ] }
+);
+
override _build_name => sub {
my $dt = DateTime->now();
return 'Electroporation Production Summary ' . $dt->ymd;
View
4 lib/LIMS2/Report/PlateList.pm
@@ -19,6 +19,10 @@ has plate_name => (
isa => 'Maybe[Str]'
);
+has '+param_names' => (
+ default => sub { [ 'plate_type', 'plate_name' ] }
+);
+
override _build_name => sub {
my $self = shift;
if ( $self->plate_type ) {
View
4 lib/LIMS2/Report/QcRun.pm
@@ -18,6 +18,10 @@ has qc_run => (
lazy_build => 1,
);
+has '+param_names' => (
+ default => sub { [ 'qc_run_id' ] }
+);
+
sub _build_qc_run {
my $self = shift;
View
4 lib/LIMS2/Report/QcRunSummary.pm
@@ -18,6 +18,10 @@ has qc_run => (
lazy_build => 1,
);
+has '+param_names' => (
+ default => sub { [ 'qc_run_id' ] }
+);
+
sub _build_qc_run {
my $self = shift;
View
4 lib/LIMS2/Report/VectorProductionSummary.pm
@@ -16,6 +16,10 @@ has species => (
required => 1
);
+has '+param_names' => (
+ default => sub { [ 'species' ] }
+);
+
override _build_name => sub {
my $dt = DateTime->now();
return 'Vector Production Summary ' . $dt->ymd;
View
61 lib/LIMS2/ReportGenerator.pm
@@ -5,6 +5,7 @@ use warnings FATAL => 'all';
use Moose;
use Iterator::Simple;
+use JSON;
use namespace::autoclean;
has name => (
@@ -27,6 +28,19 @@ has model => (
required => 1,
);
+has cache_ttl => (
+ is => 'ro',
+ isa => 'Str',
+ default => '8 hours'
+);
+
+has param_names => (
+ isa => 'ArrayRef[Str]',
+ traits => [ 'Array' ],
+ handles => { param_names => 'elements' },
+ default => sub { [] }
+);
+
sub _build_name {
confess( "_build_name() must be implemented by a subclass" );
}
@@ -50,6 +64,53 @@ sub boolean_str {
}
}
+sub cached_report {
+ my $self = shift;
+
+ my @cached = $self->model->schema->resultset('CachedReport')->search(
+ {
+ report_class => ref $self,
+ params => JSON->new->utf8->canonical->encode( $self->params_hash ),
+ expires => { '>' => \'current_timestamp' }
+ },
+ {
+ order_by => { -desc => 'expires' },
+ limit => 1
+ }
+ );
+
+ my @complete = grep { $_->complete } @cached;
+ if ( @complete ) {
+ return $complete[0];
+ }
+ elsif ( @cached ) {
+ return $cached[0];
+ }
+
+ return;
+}
+
+sub init_cached_report {
+ my ( $self, $report_id ) = @_;
+
+ my $cache_entry = $self->model->schema->resultset('CachedReport')->create(
+ {
+ id => $report_id,
+ report_class => ref $self,
+ params => JSON->new->utf8->canonical->encode( $self->params_hash ),
+ expires => \sprintf( 'current_timestamp + interval \'%s\'', $self->cache_ttl )
+ }
+ );
+
+ return $cache_entry;
+}
+
+sub params_hash {
+ my $self = shift;
+
+ return { map { $_ => $self->$_ } $self->param_names };
+}
+
__PACKAGE__->meta->make_immutable();
1;
View
4 lib/LIMS2/ReportGenerator/Plate.pm
@@ -51,6 +51,10 @@ has plate => (
}
);
+has '+param_names' => (
+ default => sub { [ 'plate_name' ] }
+);
+
sub plate_types {
confess( "plate_types() must be implemented by a subclass" );
}
View
4 lib/LIMS2/ReportGenerator/ProductionDetail.pm
@@ -19,6 +19,10 @@ has plate_type => (
lazy_build => 1
);
+has '+param_names' => (
+ default => sub { [ 'species', 'plate_type' ] }
+);
+
## no critic(RequireFinalReturn)
sub _build_plate_type {
LIMS2::Exception::Implementation->throw( "_build_plate_type() must be implemeted by a subclass" );
View
30 lib/LIMS2/WebApp/Controller/User/Report.pm
@@ -27,6 +27,36 @@ Catalyst Controller.
=cut
+=head1 GET /user/report/cache/$REPORT
+
+Retrieve a cached report. Generate the report asynchronously if there is no vaild copy in the cache.
+
+=cut
+
+sub cached_async_report :Path( '/user/report/cache' ) :Args(1) {
+ my ( $self, $c, $report ) = @_;
+
+ $c->assert_user_roles( 'read' );
+
+ my $params = $c->request->params;
+ $params->{species} ||= $c->session->{selected_species};
+
+ my $report_id = LIMS2::Report::cached_report(
+ model => $c->model( 'Golgi' ),
+ report => $report,
+ params => $params,
+ output_dir => $self->report_dir
+ );
+
+ $c->stash(
+ template => 'user/report/await_report.tt',
+ report_name => $report,
+ report_id => $report_id
+ );
+
+ return;
+}
+
=head1 GET /user/report/sync/$REPORT
Synchronously generate the report I<$REPORT>. Forward to an HTML view.
View
10 root/lib/navigation.tt
@@ -33,27 +33,27 @@
</a>
</li>
<li>
- <a href="[% c.uri_for( '/user/report/async/VectorProductionSummary' ) %]">
+ <a href="[% c.uri_for( '/user/report/cache/VectorProductionSummary' ) %]">
Vector Production Summary
</a>
</li>
<li>
- <a href="[% c.uri_for( '/user/report/async/VectorProductionDetail' ) %]">
+ <a href="[% c.uri_for( '/user/report/cache/VectorProductionDetail' ) %]">
Vector Production Detail
</a>
</li>
<li>
- <a href="[% c.uri_for( '/user/report/async/FirstElectroporationProductionDetail' ) %]">
+ <a href="[% c.uri_for( '/user/report/cache/FirstElectroporationProductionDetail' ) %]">
First Allele Electroporation Detail
</a>
</li>
<li>
- <a href="[% c.uri_for( '/user/report/async/SecondElectroporationProductionDetail' ) %]">
+ <a href="[% c.uri_for( '/user/report/cache/SecondElectroporationProductionDetail' ) %]">
Second Allele Electroporation Detail
</a>
</li>
<li>
- <a href="[% c.uri_for( '/user/report/async/ElectroporationProductionSummary' ) %]">
+ <a href="[% c.uri_for( '/user/report/cache/ElectroporationProductionSummary' ) %]">
Electroporation Production Summary
</a>
</li>
View
10 root/site/user/index.tt
@@ -33,27 +33,27 @@
</a>
</li>
<li>
- <a href="[% c.uri_for( '/user/report/async/VectorProductionSummary' ) %]">
+ <a href="[% c.uri_for( '/user/report/cache/VectorProductionSummary' ) %]">
Vector Production Summary
</a>
</li>
<li>
- <a href="[% c.uri_for( '/user/report/async/VectorProductionDetail' ) %]">
+ <a href="[% c.uri_for( '/user/report/cache/VectorProductionDetail' ) %]">
Vector Production Detail
</a>
</li>
<li>
- <a href="[% c.uri_for( '/user/report/async/FirstElectroporationProductionDetail' ) %]">
+ <a href="[% c.uri_for( '/user/report/cache/FirstElectroporationProductionDetail' ) %]">
First Allele Electroporation Detail
</a>
</li>
<li>
- <a href="[% c.uri_for( '/user/report/async/SecondElectroporationProductionDetail' ) %]">
+ <a href="[% c.uri_for( '/user/report/cache/SecondElectroporationProductionDetail' ) %]">
Second Allele Electroporation Detail
</a>
</li>
<li>
- <a href="[% c.uri_for( '/user/report/async/ElectroporationProductionSummary' ) %]">
+ <a href="[% c.uri_for( '/user/report/cache/ElectroporationProductionSummary' ) %]">
Electroporation Production Summary
</a>
</li>

No commit comments for this range

Something went wrong with that request. Please try again.