Amazon-S3-Lite
view release on metacpan or search on metacpan
lib/Amazon/S3/Lite/Lock.pm view on Meta::CPAN
s3 => $args{s3}, # Amazon::S3::Lite instance (required)
bucket => $args{bucket}, # required
key => $args{key} // 'locks/default.lock',
ttl => $args{ttl} // 120, # seconds a lock is considered fresh
owner => $args{owner} // sprintf( '%s@%s', $PID, $ENV{HOSTNAME} // 'unknown' ),
wait => $args{wait} // 0, # seconds to block waiting; 0 = no wait
poll => $args{poll} // 2, # seconds between acquire retries
}, $class;
croak 's3 is required'
if !$self->{s3};
croak 'bucket is required'
if !$self->{bucket};
return $self;
}
########################################################################
sub acquire {
########################################################################
my ($self) = @_;
# Try to create the lock only if absent. On 412 the lock is held;
# decide whether it's stale and, if so, steal it. Optionally block
# up to {wait} seconds, polling every {poll}.
my $deadline = time + $self->{wait};
while (1) {
{
my $etag = $self->_try_create; # 200 -> etag, 412 -> undef
return $self->_guard($etag) if $etag;
}
# Held. Is it stale?
my ( $status, $etag ) = $self->_try_steal_if_stale;
return $self->_guard($etag)
if $status eq 'stolen';
next
if $status eq 'vanished';
last
if time >= $deadline;
sleep $self->{poll};
}
return; # could not acquire (caller checks truthiness)
}
########################################################################
sub _try_create {
########################################################################
my ($self) = @_;
my $body = encode_json( { owner => $self->{owner}, expires => time + $self->{ttl} } );
$self->{s3}
->logger->debug( sprintf 'lock acquire: bucket=%s key=%s owner=%s', $self->{bucket}, $self->{key}, $self->{owner}, );
my $etag = eval {
$self->{s3}->put_object(
$self->{bucket}, $self->{key}, $body,
content_type => 'application/json',
headers => { 'If-None-Match' => q{*} },
);
};
my $err = $EVAL_ERROR;
$self->{s3}->logger->debug(
sprintf 'lock create: status=%s etag=%s error=%s',
$self->{s3}->last_status // q{},
$etag // q{},
$err // q{},
);
return $etag
if $self->{s3}->last_status =~ /\A2/xsm; # acquired
return
if $self->{s3}->last_status == 412; # held by someone
die $err; # real error
}
########################################################################
sub _try_steal_if_stale {
########################################################################
my ($self) = @_;
my $meta = eval { $self->{s3}->head_object( $self->{bucket}, $self->{key} ) };
$self->{s3}->logger->debug(
Dumper(
[ error => $EVAL_ERROR,
meta => $meta
]
)
);
return ('vanished')
if !$meta;
my $current_etag = $meta->{etag};
# Fetch body to read the holder's expiry (head doesn't carry it).
my $obj = eval { $self->{s3}->get_object( $self->{bucket}, $self->{key} ) };
$self->{s3}->logger->debug( Dumper( [ error => $EVAL_ERROR, ] ) );
my $data = eval { decode_json( $obj->{content} // '{}' ) } // {};
$self->{s3}->logger->debug(
Dumper(
[ error => $EVAL_ERROR,
data => $data,
]
)
);
return ('held')
if ( $data->{expires} // 0 ) > time;
# Stale. Steal ONLY if the lock is still the exact one we judged stale.
my $body = encode_json( { owner => $self->{owner}, expires => time + $self->{ttl} } );
my $etag = eval {
$self->{s3}->put_object(
$self->{bucket}, $self->{key}, $body,
content_type => 'application/json',
headers => { 'If-Match' => sprintf '"%s"', $current_etag },
);
};
$self->{s3}->logger->info( Dumper( [ error => $EVAL_ERROR, ] ) );
return ( 'stolen', $etag )
if $self->{s3}->last_status =~ /\A2/xsm;
return ('held');
}
########################################################################
sub _guard {
########################################################################
my ( $self, $etag ) = @_;
require Amazon::S3::Lite::Lock::Guard;
return Amazon::S3::Lite::Lock::Guard->new(
s3 => $self->{s3},
bucket => $self->{bucket},
key => $self->{key},
etag => $etag, # so release only deletes OUR lock
);
}
1;
( run in 0.443 second using v1.01-cache-2.11-cpan-062aa07a564 )