File: CachedTrack.pm

package info (click to toggle)
gbrowse 2.56%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 13,112 kB
  • ctags: 4,436
  • sloc: perl: 50,765; sh: 249; sql: 62; makefile: 45; ansic: 27
file content (285 lines) | stat: -rw-r--r-- 7,833 bytes parent folder | download | duplicates (7)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package Bio::Graphics::Browser2::CachedTrack;

# $Id$
# This package defines a Bio::Graphics::Browser2::Track option that manages
# the caching of track images and imagemaps.

use strict;
use warnings;
use Carp;
use Fcntl ':flock';
use File::Spec;
use File::Path;
use IO::File;
use Digest::MD5 'md5_hex';
use Storable qw(:DEFAULT freeze thaw);

# pending requests get 1 minute before they are considered likely to be defunct
use constant DEFAULT_REQUEST_TIME => 60;
use constant DEFAULT_CACHE_TIME   => 60*60; # 1 hour

# constructor:
# Bio::Graphics::Browser2::CachedTrack->new($cache_base_directory,$key_data)
# If $key_data is a scalar, then it is taken to be the literal key.
# Otherwise if it is an arrayref, it is an array of arguments that will be
# converted into the key.
sub new {
    my $self = shift;
    my %args = @_;
    my $cache_base = $args{-cache_base};
    my $panel_args = $args{-panel_args};
    my $track_args = $args{-track_args};
    my $extra_args = $args{-extra_args};
    my $cache_time = $args{-cache_time};
    my $key        = $args{-key};

    -d $cache_base && -w _ or croak "$cache_base is not writable";

    # If next argument is a scalar, then it is our key to use.
    # Otherwise, it is the data to use to generate a key.
    unless ($key) {
	$key = $self->generate_cache_key(@$panel_args,@$track_args,@$extra_args);
    }

    my $obj = bless { 
	cache_base => $cache_base ,
	key        => $key,
	panel_args => $panel_args,
	track_args => $track_args,
	extra_args => $extra_args,
	cache_time => defined $cache_time ? $cache_time : DEFAULT_CACHE_TIME,
    },ref $self || $self;
    return $obj;
}

sub cache_base { shift->{cache_base} }
sub lock_base  { shift->{lock_base} }
sub key        { shift->{key}  }
sub panel_args { shift->{panel_args} }
sub track_args { shift->{track_args} }
sub extra_args { shift->{extra_args} }
sub max_time {
    my $self = shift;
    $self->{max_time} = shift if @_;
    return $self->{max_time} || DEFAULT_REQUEST_TIME;
}
sub cache_time {
    my $self = shift;
    my $d    = $self->{cache_time};
    $self->{cache_time} = shift if @_;
    return $d;
}
sub cachedir {
    my $self = shift;
    my $key  = $self->key;
    my @comp = $key =~ /(..)/g;
    my $path = File::Spec->catfile($self->cache_base,@comp[0..2],$key);
    mkpath ($path) unless -e $path;
    die "Can't mkpath($path): $!" unless -d $path;
    return $path;
}
sub dotfile {
    my $self = shift;
    return File::Spec->catfile($self->cachedir,'.lock');
}
sub tsfile {
    my $self = shift;
    return File::Spec->catfile($self->cachedir,'.ts');
}
sub datafile {
    my $self = shift;
    return File::Spec->catfile($self->cachedir,'data');
}

sub errorfile {
    my $self = shift;
    return File::Spec->catfile($self->cachedir,'error');
}

# given an arbitrary set of arguments, make a unique cache key
sub generate_cache_key {
    my $self = shift;
    my @args = map {$_ || ''} grep {!ref($_)} @_;  # the map gets rid of uninit variable warnings
    return md5_hex(sort @args);
}

# lock the cache -- indicates that an update is in process
# we use simple dotfile locking
sub lock {
    my $self    = shift;
    my $dotfile = $self->dotfile;
    my $tsfile  = $self->tsfile;
    if (-e $dotfile) {  # if it exists, then either we are in process or something died
	return if $self->status eq 'PENDING';
    }
    my $f = IO::File->new(">$dotfile") or die "Can't open $dotfile for writing: $!";
    flock $f,LOCK_EX;
    $f->print($$,' ',time());     # PID<sp>timestamp
    $f->close;
    return 1;
}

sub unlock {
    my $self     = shift;
    my $dotfile  = $self->dotfile;
    unlink $dotfile;
}

sub flag_error {
    my $self = shift;
    my $msg  = shift;
    my $errorfile = $self->errorfile;
    open my $fh,'>',$errorfile or die;
    print $fh $msg;
    close $fh;
    $self->unlock;
}

sub errstr {
    my $self = shift;
    my $errorfile = $self->errorfile;
    open my $fh,'<',$errorfile or return;
    while (my $msg = <$fh>) {
	chomp $msg;
	next if $msg =~ /EXCEPTION/; # bioperl error header
	$msg =~ s/MSG://;            # more bioperl cruft
	return $msg if $msg;
    }
    return 'unknown';
}

sub put_data {
    my $self              = shift;
    my ($gd,$map,$titles) = @_;
    $self->{data}{gd}     = $gd->can('gd2') ? $gd->gd2 : $gd;
    $self->{data}{map}    = $map;
    $self->{data}{titles} = $titles;
    my $datafile          = $self->datafile;
    store $self->{data},$datafile;
    $self->unlock;
    unlink $self->errorfile if -e $self->errorfile;
    return;
}

sub get_data {
    my $self           = shift;
    my $ignore_expires = shift;
    return $self->{data} if $self->{data};

    my $status = $self->status;
    if ( ($status eq 'AVAILABLE') or 
	 ($status eq 'EXPIRED' && $ignore_expires)) {
	return $self->_get_data();
    } else {
	return;
    }
}

sub _get_data {
    my $self = shift;
    my $datafile  = $self->datafile;
    $self->{data} = retrieve($datafile);
    return $self->{data};
}

sub gd {
    my $self = shift;
    my $data = $self->get_data or return;

    # The ? statement here accomodates the storage of GD::SVG objects,
    # which do not support the call to newFromPngData.
    my $gd = (ref($data->{gd}) 
	    && ref($data->{gd})=~/^GD/)
	? $data->{gd}
        : GD::Image->newFromGd2Data($data->{gd});
    return $gd;
}

sub map {
    my $self = shift;
    my $data = $self->get_data or return;
    return $data->{map};
}

sub titles {
    my $self = shift;
    my $data = $self->get_data or return;
    return $data->{titles};
}

sub width {
    my $self = shift;
    my $gd   = $self->gd or return;
    return ($gd->getBounds)[0];
}

sub height {
    my $self = shift;
    my $gd   = $self->gd or return;
    return ($gd->getBounds)[1];
}

# status returns one of four states
# 'EMPTY'     no data available and no requests are pending
# 'PENDING'   a request for the data is pending - current contents invalid
# 'AVAILABLE' data is available and no requests are pending
# 'DEFUNCT'   a request for the data has timed out - current contents invalid
# 'EXPIRED'   there is data, but it has expired
# 'ERROR'     an error occurred, and data will never be available
sub status {
    my $self      = shift;
    my $dir       = $self->cachedir;
    my $dotfile   = $self->dotfile;
    my $tsfile    = $self->tsfile;
    my $datafile  = $self->datafile;
    my $errorfile = $self->errorfile;

    # if a dotfile exists then either we are in the midst of updating the
    # contents of the directory, or something has gone wrong and we are
    # waiting forever.
    if (-e $dotfile) {
	-s _ or return 'PENDING';  # size zero means that dotfile has been created but not locked
	my $f = IO::File->new($dotfile) 
	    or return 'AVAILABLE'; # dotfile disappeared, so data has just become available
	flock $f,LOCK_SH;
	my ($pid,$timestamp) = split /\s+/,$f->getline();
	$f->close;
	return 'DEFUNCT' unless $timestamp;
	unless (kill 0=>$pid) {
	    $self->flag_error('the rendering process crashed');
	    return 'ERROR';
	}
	return 'PENDING' if time()-$timestamp < $self->max_time;
	$self->flag_error('timeout; try viewing a smaller region');
	return 'ERROR';
    } elsif (-e $datafile) {
	return $self->expired($datafile) ? 'EXPIRED' : 'AVAILABLE';
    } elsif (-e $errorfile) {
	return 'ERROR';
    } else {
	return 'EMPTY';
    }
}

sub needs_refresh {
    my $self   = shift;
    my $status = $self->status;
    return 1 if $status eq 'EMPTY';
    return 1 if $status eq 'EXPIRED';
    return 1 if $status eq 'DEFUNCT';
    return;
}

sub expired {
    my $self      = shift;
    my $datafile  = shift;
    my $cache_time= $self->cache_time;
    my $time      = time();

    my $mtime    = (stat($datafile))[9];
    my $elapsed  = $time-$mtime;
    return 0 if ( $mtime and not $cache_time);
    return $elapsed > $cache_time;
}

1;