File Coverage

blib/lib/Bot/Cobalt/Plugin/RDB/Database.pm
Criterion Covered Total %
statement 9 11 81.8
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 13 15 86.6


line stmt bran cond sub pod time code
1             package Bot::Cobalt::Plugin::RDB::Database;
2             $Bot::Cobalt::Plugin::RDB::Database::VERSION = '0.021002';
3             ## Frontend to managing RDB-style Bot::Cobalt::DB instances
4             ## I regret writing this.
5             ##
6             ## We may have a lot of RDBs.
7             ## This plugin tries to make it easy to operate on them discretely
8             ## with a minimum of angst in the frontend app.
9             ##
10             ## If there is no DB in our RDBDir named 'main' it is initialized.
11             ##
12             ## If an error occurs, the first argument returned will be boolean false.
13             ## The error as a simple string is available via the 'Error' method.
14             ## These values are only 'sort-of' human readable; they're holdovers from
15             ## the previous constant retvals, and typically translated into langset
16             ## RPLs by Plugin::RDB.
17             ##
18             ## Our RDB interfaces typically take RDB names; we map them to paths and
19             ## attempt to switch our ->{CURRENT} Bot::Cobalt::DB object appropriately.
20             ##
21             ## The frontend doesn't have to worry about dbopen/dbclose, which works
22             ## for RDBs because access is almost always a single operation and we
23             ## can afford to open / lock / access / unlock / close every call.
24              
25 2     2   1068 use v5.10;
  2         6  
26 2     2   8 use strictures 2;
  2         10  
  2         58  
27              
28 2     2   295 use Carp;
  2         4  
  2         108  
29              
30 2     2   409 use Bot::Cobalt::DB;
  0            
  0            
31             use Bot::Cobalt::Error;
32             use Bot::Cobalt::Utils qw/ glob_to_re_str /;
33              
34             use Bot::Cobalt::Plugin::RDB::SearchCache;
35              
36             use Path::Tiny;
37             use List::Util qw/shuffle/;
38             use Time::HiRes;
39             use Try::Tiny;
40              
41             sub new {
42             my $self = {};
43             my $class = shift;
44             bless $self, $class;
45              
46             my %opts = @_;
47            
48             my $core;
49            
50             if (ref $opts{core}) {
51             $core = delete $opts{core};
52             } else {
53             require Bot::Cobalt::Core;
54             $core = Bot::Cobalt::Core->instance;
55             }
56              
57             $self->{core} = $core;
58              
59             my $rdbdir = path(
60             delete $opts{RDBDir} || croak "new() needs a RDBDir"
61             );
62              
63             $self->{RDBDir} = $rdbdir;
64            
65             $self->{CacheObj} = Bot::Cobalt::Plugin::RDB::SearchCache->new(
66             MaxKeys => $opts{CacheKeys} // 30,
67             );
68            
69             $core->log->debug("Using RDBDir $rdbdir");
70              
71            
72             unless ($rdbdir->exists) {
73             $core->log->debug("Did not find RDBDir $rdbdir, attempting mkpath");
74             $rdbdir->mkpath;
75             }
76              
77             unless ($rdbdir->is_dir) {
78             confess "Found RDBDir $rdbdir but it is not a directory!";
79             }
80            
81             unless ( $self->dbexists('main') ) {
82             $core->log->debug("No main RDB found, creating one");
83              
84             try {
85             $self->createdb('main')
86             } catch {
87             $core->log->warn("Failed to create 'main' RDB: $_")
88             };
89             }
90            
91             return $self
92             }
93              
94             sub dbexists {
95             my ($self, $rdb) = @_;
96             $self->path_from_name($rdb)->exists
97             }
98              
99             sub path_from_name {
100             my ($self, $rdb) = @_;
101             path( $self->{RDBDir} .'/'. $rdb .'.rdb' )
102             }
103              
104             sub error {
105             my ($self, $error) = @_;
106             Bot::Cobalt::Error->new( $error )
107             }
108              
109             sub createdb {
110             my ($self, $rdb) = @_;
111            
112             die $self->error("RDB_INVALID_NAME")
113             unless $rdb and $rdb =~ /^[A-Za-z0-9]+$/;
114              
115             die $self->error("RDB_EXISTS")
116             if $self->dbexists($rdb);
117            
118             my $core = $self->{core};
119             $core->log->debug("attempting to create RDB $rdb");
120            
121             my $path = $self->path_from_name($rdb);
122              
123             $self->_rdb_switch($rdb);
124             my $db = $self->{CURRENT};
125              
126             unless ( ref $db ) {
127             $core->log->error("Could not switch to RDB $rdb at $path");
128             die $self->error("RDB_DBFAIL")
129             }
130            
131             unless ( $db->dbopen ) {
132             $core->log->error("dbopen failure for $rdb in createdb");
133             die $self->error("RDB_DBFAIL")
134             }
135            
136             $db->dbclose;
137            
138             $core->log->info("Created RDB $rdb");
139            
140             return 1
141             }
142              
143             sub deldb {
144             my ($self, $rdb) = @_;
145             confess "No RDB specified" unless defined $rdb;
146              
147             my $core = $self->{core};
148              
149             die $self->error("RDB_INVALID_NAME")
150             unless $rdb and $rdb =~ /^[A-Za-z0-9]+$/;
151              
152             die $self->error("RDB_NOSUCH")
153             unless $self->dbexists($rdb);
154            
155             $self->_rdb_switch($rdb);
156             my $db = $self->{CURRENT};
157              
158             unless ( ref $db ) {
159             $core->log->error("deldb failure; cannot switch to $rdb");
160             die $self->error("RDB_DBFAIL")
161             }
162            
163             unless ( $db->dbopen ) {
164             $core->log->error("dbopen failure for $rdb in deldb");
165             $core->log->error("Refusing to unlink, admin should investigate.");
166             die $self->error("RDB_DBFAIL")
167             }
168              
169             $db->dbclose;
170              
171             $self->{CURRENT} = undef;
172              
173             undef $db;
174              
175             my $cache = $self->{CacheObj};
176             $cache->invalidate($rdb);
177              
178             my $path = $self->path_from_name($rdb);
179             unless ( unlink $path ) {
180             $core->log->error("Cannot unlink RDB $rdb at $path: $!");
181             die $self->error("RDB_FILEFAILURE")
182             }
183            
184             $core->log->info("Deleted RDB $rdb");
185            
186             return 1
187             }
188              
189             sub del {
190             my ($self, $rdb, $key) = @_;
191             confess "No RDB specified" unless defined $rdb;
192              
193             my $core = $self->{core};
194              
195             die $self->error("RDB_INVALID_NAME")
196             unless $rdb and $rdb =~ /^[A-Za-z0-9]+$/;
197            
198             die $self->error("RDB_NOSUCH")
199             unless $self->dbexists($rdb);
200            
201             $self->_rdb_switch($rdb);
202             my $db = $self->{CURRENT};
203            
204             unless ( ref $db ) {
205             $core->log->error("del failure; cannot switch to $rdb");
206             die $self->error("RDB_DBFAIL")
207             }
208            
209             unless ( $db->dbopen ) {
210             $core->log->error("dbopen failure for $rdb in del");
211             die $self->error("RDB_DBFAIL")
212             }
213            
214             unless ( $db->get($key) ) {
215             $db->dbclose;
216            
217             $core->log->debug("no such item: $key in $rdb");
218            
219             die $self->error("RDB_NOSUCH_ITEM")
220             }
221            
222             unless ( $db->del($key) ) {
223             $db->dbclose;
224              
225             $core->log->warn("failure in db->del for $key in $rdb");
226              
227             die $self->error("RDB_DBFAIL")
228             }
229            
230             my $cache = $self->{CacheObj};
231             $cache->invalidate($rdb);
232            
233             $db->dbclose;
234             return 1
235             }
236              
237             sub get {
238             my ($self, $rdb, $key) = @_;
239             confess "No RDB specified" unless defined $rdb;
240              
241             my $core = $self->{core};
242              
243             die $self->error("RDB_INVALID_NAME")
244             unless $rdb and $rdb =~ /^[A-Za-z0-9]+$/;
245              
246             die $self->error("RDB_NOSUCH")
247             unless $self->dbexists($rdb);
248            
249             $self->_rdb_switch($rdb);
250             my $db = $self->{CURRENT};
251            
252             unless ( ref $db ) {
253             $core->log->error("get failure; cannot switch to $rdb");
254             die $self->error("RDB_DBFAIL")
255             }
256            
257             unless ( $db->dbopen(ro => 1) ) {
258             $core->log->error("dbopen failure for $rdb in get");
259             die $self->error("RDB_DBFAIL")
260             }
261            
262             my $value = $db->get($key);
263             unless ( defined $value ) {
264             $db->dbclose;
265             die $self->error("RDB_NOSUCH_ITEM")
266             }
267            
268             $db->dbclose;
269            
270             return $value
271             }
272              
273             sub get_keys {
274             my ($self, $rdb) = @_;
275             confess "No RDB specified" unless defined $rdb;
276              
277             die $self->error("RDB_INVALID_NAME")
278             unless $rdb and $rdb =~ /^[A-Za-z0-9]+$/;
279              
280             die $self->error("RDB_NOSUCH")
281             unless $self->dbexists($rdb);
282              
283             my $core = $self->{core};
284            
285             $self->_rdb_switch($rdb);
286             my $db = $self->{CURRENT};
287              
288             unless ( ref $db ) {
289             $core->log->error("get_keys failure; cannot switch to $rdb");
290             die $self->error("RDB_DBFAIL")
291             }
292            
293             unless ( $db->dbopen(ro => 1) ) {
294             $core->log->error("dbopen failure for $rdb in get_keys");
295             die $self->error("RDB_DBFAIL")
296             }
297            
298             my @dbkeys = $db->dbkeys;
299             $db->dbclose;
300              
301             return wantarray ? @dbkeys : scalar(@dbkeys)
302             }
303              
304             sub put {
305             my ($self, $rdb, $ref) = @_;
306              
307             confess "put() needs a RDB name and a reference"
308             unless defined $rdb and defined $ref;
309            
310             die $self->error("RDB_INVALID_NAME")
311             unless $rdb and $rdb =~ /^[A-Za-z0-9]+$/;
312              
313             die $self->error("RDB_NOSUCH")
314             unless $self->dbexists($rdb);
315            
316             my $core = $self->{core};
317            
318             $self->_rdb_switch($rdb);
319             my $db = $self->{CURRENT};
320              
321             unless ( ref $db ) {
322             $core->log->error("put failure; cannot switch to $rdb");
323             die $self->error("RDB_DBFAIL")
324             }
325            
326             unless ( $db->dbopen ) {
327             $core->log->error("dbopen failure for $rdb in put");
328             die $self->error("RDB_DBFAIL")
329             }
330            
331             my $newkey = $self->_gen_unique_key;
332            
333             unless ( $db->put($newkey, $ref) ) {
334             $db->dbclose;
335             die $self->error("RDB_DBFAIL")
336             }
337              
338             $db->dbclose;
339            
340             my $cache = $self->{CacheObj};
341             $cache->invalidate($rdb);
342              
343             return $newkey
344             }
345              
346             sub random {
347             my ($self, $rdb) = @_;
348             confess "No RDB specified" unless defined $rdb;
349            
350             die $self->error("RDB_INVALID_NAME")
351             unless $rdb and $rdb =~ /^[A-Za-z0-9]+$/;
352              
353             die $self->error("RDB_NOSUCH")
354             unless $self->dbexists($rdb);
355              
356             my $core = $self->{core};
357            
358             $self->_rdb_switch($rdb);
359             my $db = $self->{CURRENT};
360              
361             unless ( ref $db ) {
362             $core->log->error("random failure; cannot switch to $rdb");
363             die $self->error("RDB_DBFAIL")
364             }
365            
366             unless ( $db->dbopen(ro => 1) ) {
367             $core->log->error("dbopen failure for $rdb in random");
368             die $self->error("RDB_DBFAIL")
369             }
370            
371             my @dbkeys = $db->dbkeys;
372             unless (@dbkeys) {
373             $db->dbclose;
374             die $self->error("RDB_NOSUCH_ITEM")
375             }
376            
377             my $randkey = $dbkeys[rand @dbkeys];
378             my $ref = $db->get($randkey);
379              
380             unless (ref $ref) {
381             $db->dbclose;
382             $core->log->error("Broken DB? item $randkey in $rdb not a ref");
383             die $self->error("RDB_DBFAIL");
384             }
385             $db->dbclose;
386            
387             return $ref
388             }
389              
390             sub search {
391             my ($self, $rdb, $glob, $wantone) = @_;
392             confess "search() needs a RDB name and a glob"
393             unless defined $rdb and defined $glob;
394              
395             die $self->error("RDB_INVALID_NAME")
396             unless $rdb and $rdb =~ /^[A-Za-z0-9]+$/;
397              
398             die $self->error("RDB_NOSUCH")
399             unless $self->dbexists($rdb);
400              
401             my $core = $self->{core};
402            
403             $self->_rdb_switch($rdb);
404             my $db = $self->{CURRENT};
405              
406             unless ( ref $db ) {
407             $core->log->error("search failure; cannot switch to $rdb");
408             die $self->error("RDB_DBFAIL")
409             }
410            
411             ## hit search cache first
412             my $cache = $self->{CacheObj};
413             my @matches = $cache->fetch($rdb, $glob);
414             if (@matches) {
415             if ($wantone) {
416             return (shuffle @matches)[-1]
417             } else {
418             return wantarray ? @matches : [ @matches ]
419             }
420             }
421              
422             my $re = glob_to_re_str($glob);
423             $re = qr/$re/i;
424              
425             unless ( $db->dbopen(ro => 1) ) {
426             $core->log->error("dbopen failure for $rdb in search");
427             die $self->error("RDB_DBFAIL")
428             }
429            
430             my @dbkeys = $db->dbkeys;
431             for my $dbkey (shuffle @dbkeys) {
432             my $ref = $db->get($dbkey) // next;
433             my $str = ref $ref eq 'HASH' ? $ref->{String} : $ref->[0] ;
434              
435             if ($str =~ $re) {
436             if ($wantone) {
437             ## plugin only cares about one match, short-circuit
438             $db->dbclose;
439             return $dbkey
440             } else {
441             push(@matches, $dbkey);
442             }
443             }
444              
445             }
446            
447             $db->dbclose;
448              
449             ## WANTONE but we didn't find any, return undef
450             return undef if $wantone;
451            
452             ## push back to cache
453             $cache->cache($rdb, $glob, [ @matches ] );
454            
455             return wantarray ? @matches : [ @matches ]
456             }
457              
458              
459             sub cache_check {
460             my ($self, $rdb, $glob) = @_;
461             my $cache = $self->{CacheObj};
462            
463             my @matches = $cache->fetch($rdb, $glob);
464             return @matches
465             }
466              
467             sub cache_push {
468             my ($self, $rdb, $glob, $ref) = @_;
469             my $cache = $self->{CacheObj};
470            
471             $cache->cache($rdb, $glob, $ref);
472             }
473              
474             sub _gen_unique_key {
475             my ($self) = @_;
476              
477             my $db = $self->{CURRENT}
478             || croak "_gen_unique_key called but no db to check";
479              
480             my @v = ( 'a' .. 'f', 0 .. 9 );
481             my $newkey = join '', map { $v[rand @v] } 1 .. 4;
482             $newkey .= $v[rand @v] while exists $db->Tied->{$newkey};
483              
484             ## regen 0000 keys:
485             $newkey =~ /^0+$/ ? $self->_gen_unique_key : $newkey
486             }
487              
488             sub _rdb_switch {
489             my ($self, $rdb) = @_;
490            
491             undef $self->{CURRENT};
492            
493             my $core = $self->{core};
494             my $path = $self->path_from_name($rdb);
495             unless ($path) {
496             $core->log->error("_rdb_switch failed; no path for $rdb");
497             return
498             }
499            
500             $self->{CURRENT} = Bot::Cobalt::DB->new(
501             File => $path,
502             );
503             }
504              
505             1;
506             __END__