package Qpsmtpd::DB::Redis; use strict; use warnings; use parent 'Qpsmtpd::DB'; sub new { my ( $class, %args ) = @_; my $self = bless {}, $class; $self->name( delete $args{name} ) if defined $args{name}; $self->{redis_args} = {%args}; $self->init_db(); return $self; } sub init_redis { my ( $self ) = @_; # Stringy eval needed to allow 'use Qpmstpd::DB::Redis' to succeed # even when Redis module is unavailable; mainly for testing eval 'use Redis'; die $@ if $@; my $redis = $self->{redis} = MyRedis->new( %{ $self->{redis_args} } ); $redis->selected(0); return $redis; } sub init_db { my ( $self ) = @_; my $redis = $self->init_redis; return if $redis->get('___smtpd_reserved___'); die "Redis DB at index 0 is already populated!" if $redis->dbsize; $redis->set( ___smtpd_reserved___ => 1 ); } sub redis { my ( $self, $index ) = @_; my $redis = $self->{redis} or die "redis(): redis was not initialized"; $index = $self->index if ! defined $index; $redis->select( $index ); return $redis; } sub index { # Get index of database where the current plugin's data should be stored my ( $self ) = @_; return $self->{index} if $self->{index}; my $redis = $self->redis(0); my %stores = $redis->hgetall('smtpd_stores'); return $self->{index} = $stores{ $self->name } if $stores{ $self->name }; my %rstores = reverse %stores; for my $index ( 1 .. 255 ) { $redis->select($index); # This index is earmarked for something else next if exists $rstores{$index}; # This index is populated by something else next if $redis->dbsize; # We can populate this empty store $self->redis(0)->hset( 'smtpd_stores', $self->name => $index ); return $self->{index} = $index; } } sub get { my ( $self, $key ) = @_; if ( ! $key ) { warn "No key provided, get() failed\n"; return; } return $self->redis->get($key); } sub mget { my ( $self, @keys ) = @_; if ( ! @keys ) { warn "No key provided, mget() failed\n"; return; } return $self->redis->mget(@keys); } sub set { my ( $self, $key, $val ) = @_; if ( ! $key ) { warn "No key provided, set() failed\n"; return; } return $self->redis->set( $key, $val ); } sub delete { my ( $self, @keys ) = @_; if ( ! @keys ) { warn "No key provided, delete() failed\n"; return; } return $self->redis->del(@keys); } sub get_keys { shift->redis->keys('*') } sub size { shift->redis->dbsize } sub flush { shift->redis->flushdb } package MyRedis; eval "use parent 'Redis'"; # With all the (necessary) redundant select() going on, let's track the # currently selected db and avoid the round trip when select() is a noop sub select { my $self = shift; my ( $index ) = @_; return if $index == $self->selected; my $r = $self->SUPER::select(@_); $self->selected( $index ); return $r; } sub selected { my ( $self, $index ) = @_; $self->{selected} = $index if defined $index; return $self->{selected} if defined $self->{selected}; return -1; } 1;