216 lines
7.2 KiB
Perl
216 lines
7.2 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# DW::BlobStore
|
||
|
|
#
|
||
|
|
# Meta storage API for storing arbitrary blobs of content by key.
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Mark Smith <mark@dreamwidth.org>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2016-2018 by Dreamwidth Studios, LLC.
|
||
|
|
#
|
||
|
|
# This program is free software; you may redistribute it and/or modify it under
|
||
|
|
# the same terms as Perl itself. For a copy of the license, please reference
|
||
|
|
# 'perldoc perlartistic' or 'perldoc perlgpl'.
|
||
|
|
#
|
||
|
|
|
||
|
|
package DW::BlobStore;
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use v5.10;
|
||
|
|
use Log::Log4perl;
|
||
|
|
my $log = Log::Log4perl->get_logger(__PACKAGE__);
|
||
|
|
|
||
|
|
use File::Temp;
|
||
|
|
|
||
|
|
use DW::Stats;
|
||
|
|
use LJ::ModuleLoader;
|
||
|
|
|
||
|
|
LJ::ModuleLoader->require_subclasses('DW::BlobStore');
|
||
|
|
|
||
|
|
my $blobstores;
|
||
|
|
|
||
|
|
sub _get_blobstores {
|
||
|
|
|
||
|
|
# If we've already created one, simply return it.
|
||
|
|
return $blobstores if defined $blobstores;
|
||
|
|
|
||
|
|
# If we're in the middle of a test, create a new temporary directory and set
|
||
|
|
# # up localdisk only.
|
||
|
|
if ( LJ::in_test() ) {
|
||
|
|
my $dir = File::Temp::tempdir( CLEANUP => 1 );
|
||
|
|
$blobstores = [ DW::BlobStore::LocalDisk->init( path => $dir ) ];
|
||
|
|
return $blobstores;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $idx = 0;
|
||
|
|
while ( $idx < scalar @LJ::BLOBSTORES ) {
|
||
|
|
my ( $name, $config ) = @LJ::BLOBSTORES[ $idx, $idx + 1 ];
|
||
|
|
$log->logcroak('Value must be a hashref.')
|
||
|
|
unless $config && ref $config eq 'HASH';
|
||
|
|
|
||
|
|
if ( $name eq 'localdisk' ) {
|
||
|
|
push @{ $blobstores ||= [] }, DW::BlobStore::LocalDisk->init(%$config);
|
||
|
|
}
|
||
|
|
elsif ( $name eq 'mogilefs' ) {
|
||
|
|
push @{ $blobstores ||= [] }, DW::BlobStore::MogileFS->init(%$config);
|
||
|
|
}
|
||
|
|
elsif ( $name eq 's3' ) {
|
||
|
|
push @{ $blobstores ||= [] }, DW::BlobStore::S3->init(%$config);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$log->logcroak( 'Invalid blobstore type: ' . $name );
|
||
|
|
}
|
||
|
|
|
||
|
|
$idx += 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
$log->logcroak('Must configure @LJ::BLOBSTORES.')
|
||
|
|
unless $blobstores;
|
||
|
|
$log->debug( 'Blobstore initialized with ', scalar(@$blobstores), ' blobstores.' );
|
||
|
|
return $blobstores;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Check validity of namespace. Dies if invalid.
|
||
|
|
sub ensure_namespace_is_valid {
|
||
|
|
my ($namespace) = @_;
|
||
|
|
|
||
|
|
# Ensure that namespace is alpha-numeric
|
||
|
|
unless ( $namespace =~ m!^(?:[a-z][a-z0-9]+)$! ) {
|
||
|
|
DW::Stats::increment( 'dw.blobstore.error.namespace_invalid', 1 );
|
||
|
|
$log->logcroak("Namespace '$namespace' is invalid.");
|
||
|
|
}
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Check validity of key. Dies if key is invalid.
|
||
|
|
sub ensure_key_is_valid {
|
||
|
|
my ($key) = @_;
|
||
|
|
|
||
|
|
# This is just a check to ensure that nobody uses a key without path
|
||
|
|
# elements or with invalid characters.
|
||
|
|
unless ( $key =~ m!^(?:[a-z0-9]+[_:/-]+)+([a-z0-9]+)$! ) {
|
||
|
|
DW::Stats::increment( 'dw.blobstore.error.key_invalid', 1 );
|
||
|
|
$log->logcroak("Key '$key' is invalid.");
|
||
|
|
}
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Store a file. File must be a scalarref. Return value is 1 if it was stored somewhere,
|
||
|
|
# and 0 if not. File will be stored to only one store.
|
||
|
|
sub store {
|
||
|
|
my ( $class, $namespace, $key, $blobref ) = @_;
|
||
|
|
ensure_namespace_is_valid($namespace);
|
||
|
|
ensure_key_is_valid($key);
|
||
|
|
$log->logcroak('Store requires data be a scalar reference.')
|
||
|
|
unless ref $blobref eq 'SCALAR';
|
||
|
|
$log->debug("Meta-blobstore: storing ($namespace, $key)");
|
||
|
|
|
||
|
|
# Storage requests always go to the first blobstore that will take them,
|
||
|
|
# we never store something twice.
|
||
|
|
foreach my $bs ( @{ $class->_get_blobstores } ) {
|
||
|
|
my $rv = $bs->store( $namespace, $key, $blobref );
|
||
|
|
if ($rv) {
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.store_ok', 1, [ 'store:' . $bs->type ] );
|
||
|
|
return $rv;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.store_failed', 1, [ 'store:' . $bs->type ] );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$log->info("Meta-blobstore: failed to store ($namespace, $key)");
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.store_error', 1 );
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Delete a file from ALL known stores. Return 1 if it was deleted at least once,
|
||
|
|
# else return 0.
|
||
|
|
sub delete {
|
||
|
|
my ( $class, $namespace, $key ) = @_;
|
||
|
|
ensure_namespace_is_valid($namespace);
|
||
|
|
ensure_key_is_valid($key);
|
||
|
|
$log->debug("Meta-blobstore: deleting ($namespace, $key)");
|
||
|
|
|
||
|
|
# Deletes must be sent to all blobstores. Return true if any accepted
|
||
|
|
# the delete.
|
||
|
|
my $rv = 0;
|
||
|
|
foreach my $bs ( @{ $class->_get_blobstores } ) {
|
||
|
|
$rv = $bs->delete( $namespace, $key ) || $rv;
|
||
|
|
}
|
||
|
|
if ($rv) {
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.delete_ok', 1 );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
# No 'failed' stat, delete operations can only fail entirely and not per-store since
|
||
|
|
# we are for sure sending deletes to all stores
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.delete_error', 1 );
|
||
|
|
}
|
||
|
|
return $rv;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Retrieves a file from the blobstore. May return either a scalar-ref if the file
|
||
|
|
# was found, or returns undef.
|
||
|
|
sub retrieve {
|
||
|
|
my ( $class, $namespace, $key ) = @_;
|
||
|
|
ensure_namespace_is_valid($namespace);
|
||
|
|
ensure_key_is_valid($key);
|
||
|
|
$log->debug("Meta-blobstore: retrieving ($namespace, $key)");
|
||
|
|
|
||
|
|
# Try blobstores in priority order.
|
||
|
|
my $num_failures = 0;
|
||
|
|
foreach my $bs ( @{ $class->_get_blobstores } ) {
|
||
|
|
my $rv = $bs->retrieve( $namespace, $key );
|
||
|
|
if ($rv) {
|
||
|
|
if ( $num_failures == 1 ) {
|
||
|
|
|
||
|
|
# If we're in a migration, we often expect to see one failure followed by a
|
||
|
|
# success. In that case, we want to cascade a store off of this retrieve to
|
||
|
|
# store the file.
|
||
|
|
$log->info("Meta-blobstore: cascading store for ($namespace, $key)");
|
||
|
|
if ( $class->store( $namespace => $key, $rv ) ) {
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.retrieve_cascade_ok', 1 );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.retrieve_cascade_error', 1 );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.retrieve_ok', 1, [ 'store:' . $bs->type ] );
|
||
|
|
return $rv;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$num_failures++;
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.retrieve_failed',
|
||
|
|
1, [ 'store:' . $bs->type ] );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$log->info("Meta-blobstore: failed to retrieve ($namespace, $key)");
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.retrieve_error', 1 );
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Check if a file exists in any defined store. Returns 1 if it does, 0 if not.
|
||
|
|
sub exists {
|
||
|
|
my ( $class, $namespace, $key ) = @_;
|
||
|
|
ensure_namespace_is_valid($namespace);
|
||
|
|
ensure_key_is_valid($key);
|
||
|
|
$log->debug("Meta-blobstore: checking if exists ($namespace, $key)");
|
||
|
|
|
||
|
|
# Try blobstores in priority order.
|
||
|
|
foreach my $bs ( @{ $class->_get_blobstores } ) {
|
||
|
|
my $rv = $bs->exists( $namespace, $key );
|
||
|
|
if ($rv) {
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.exists_ok', 1, [ 'store:' . $bs->type ] );
|
||
|
|
return $rv;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.exists_failed', 1,
|
||
|
|
[ 'store:' . $bs->type ] );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$log->info("Meta-blobstore: file doesn't exist in any store ($namespace, $key)");
|
||
|
|
DW::Stats::increment( 'dw.blobstore.action.exists_error', 1 );
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
1;
|