1 # Copyright (C) all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4 # fork()-friendly key-value store. Will be used for making
5 # augmenting Maildirs and mboxes less expensive, maybe.
6 # We use flock(2) to avoid SQLite lock problems (busy timeouts, backoff)
7 package PublicInbox::SharedKV;
10 use parent qw(PublicInbox::Lock);
11 use File::Temp qw(tempdir);
12 use DBI qw(:sql_types); # SQL_BLOB
13 use PublicInbox::Spawn;
14 use File::Path qw(rmtree make_path);
17 my ($self, $lock) = @_;
19 my $f = $self->{filename};
20 $lock //= $self->lock_for_scope_fast;
21 my $dbh = DBI->connect("dbi:SQLite:dbname=$f", '', '', {
25 sqlite_use_immediate_transaction => 1,
26 # no sqlite_unicode here, this is for binary data
28 my $opt = $self->{opt} // {};
29 $dbh->do('PRAGMA synchronous = OFF') if !$opt->{fsync};
30 $dbh->do('PRAGMA journal_mode = '.
31 ($opt->{journal_mode} // 'WAL'));
33 CREATE TABLE IF NOT EXISTS kv (
34 k VARBINARY PRIMARY KEY NOT NULL,
44 my ($cls, $dir, $base, $opt) = @_;
45 my $self = bless { opt => $opt }, $cls;
46 make_path($dir) if defined($dir) && !-d $dir;
47 $dir //= $self->{"tmp$$.$self"} = tempdir("skv.$$-XXXX", TMPDIR => 1);
49 my $f = $self->{filename} = "$dir/$base.sqlite3";
50 $self->{lock_path} = $opt->{lock_path} // "$dir/$base.flock";
52 require PublicInbox::Syscall;
53 PublicInbox::Syscall::nodatacow_dir($dir); # for journal/shm/wal
54 open my $fh, '+>>', $f or die "failed to open $f: $!";
60 my ($self, $key, $val, $lock) = @_;
61 $lock //= $self->lock_for_scope_fast;
62 my $sth = $self->{dbh}->prepare_cached(<<'');
63 INSERT OR IGNORE INTO kv (k,v) VALUES (?, ?)
65 $sth->bind_param(1, $key, SQL_BLOB);
66 $sth->bind_param(2, $val, SQL_BLOB);
67 my $e = $sth->execute;
71 # caller calls sth->fetchrow_array
74 my $sth = $self->{dbh}->prepare_cached(<<'', undef, 1);
82 my ($self, @pfx) = @_;
83 my $sql = 'SELECT k FROM kv';
84 if (defined $pfx[0]) {
85 $sql .= ' WHERE k LIKE ? ESCAPE ?';
86 my $anywhere = !!$pfx[1];
88 $pfx[0] =~ s/([%_\\])/\\$1/g; # glob chars
90 substr($pfx[0], 0, 0, '%') if $anywhere;
92 @pfx = (); # [0] may've been undef
94 my $sth = $self->dbh->prepare($sql);
96 $sth->bind_param(1, $pfx[0], SQL_BLOB);
97 $sth->bind_param(2, $pfx[1]);
100 map { $_->[0] } @{$sth->fetchall_arrayref};
104 my ($self, $key, $val) = @_;
106 my $sth = $self->{dbh}->prepare_cached(<<'');
107 INSERT OR REPLACE INTO kv (k,v) VALUES (?,?)
109 $sth->bind_param(1, $key, SQL_BLOB);
110 $sth->bind_param(2, $val, SQL_BLOB);
111 my $e = $sth->execute;
112 $e == 0 ? undef : $e;
114 my $sth = $self->{dbh}->prepare_cached(<<'');
115 DELETE FROM kv WHERE k = ?
117 $sth->bind_param(1, $key, SQL_BLOB);
122 my ($self, $key) = @_;
123 my $sth = $self->{dbh}->prepare_cached(<<'', undef, 1);
124 SELECT v FROM kv WHERE k = ?
126 $sth->bind_param(1, $key, SQL_BLOB);
128 $sth->fetchrow_array;
132 my ($self, $key, $newval, $lock) = @_;
133 $lock //= $self->lock_for_scope_fast;
134 my $oldval = get($self, $key);
135 if (defined $newval) {
136 set($self, $key, $newval);
138 my $sth = $self->{dbh}->prepare_cached(<<'');
139 DELETE FROM kv WHERE k = ?
141 $sth->bind_param(1, $key, SQL_BLOB);
149 my $sth = $self->{dbh}->prepare_cached(<<'');
150 SELECT COUNT(k) FROM kv
153 $sth->fetchrow_array;
156 # faster than ->count due to how SQLite works
159 my @n = $self->{dbh}->selectrow_array('SELECT k FROM kv LIMIT 1');
160 scalar(@n) ? 1 : undef;
164 my ($self, $lock) = @_;
165 my $dbh = delete $self->{dbh} or return;
166 $lock //= $self->lock_for_scope_fast; # may be needed for WAL
167 %{$dbh->{CachedKids}} = (); # cleanup prepare_cached
174 my $dir = delete $self->{"tmp$$.$self"} or return;
178 eval { rmtree($dir) };
179 } while ($@ && $!{ENOENT} && $tries++ < 5);
180 warn "error removing $dir: $@" if $@;
181 warn "Took $tries tries to remove $dir\n" if $tries;