#!/usr/bin/perl # # DW::BlobStore # # Meta storage API for storing arbitrary blobs of content by key. # # Authors: # Mark Smith # # 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;