dumping ground for random patches and texts
 help / color / mirror / Atom feed
* [PATCH] work-in-progress: crit-bit map
@ 2015-09-06  3:03 Eric Wong
  0 siblings, 0 replies; only message in thread
From: Eric Wong @ 2015-09-06  3:03 UTC (permalink / raw)
  To: spew

Maybe I don't want to depend on SQLite?
---
 lib/PublicInbox/CBM.pm | 339 +++++++++++++++++++++++++++++++++++++++++++++++++
 t/cbm.t                |  25 ++++
 2 files changed, 364 insertions(+)
 create mode 100644 lib/PublicInbox/CBM.pm
 create mode 100644 t/cbm.t

diff --git a/lib/PublicInbox/CBM.pm b/lib/PublicInbox/CBM.pm
new file mode 100644
index 0000000..7ab583f
--- /dev/null
+++ b/lib/PublicInbox/CBM.pm
@@ -0,0 +1,339 @@
+# CC0 (Public domain): http://creativecommons.org/publicdomain/zero/1.0/
+# Note: the rest of public-inbox is AGPLv3 <meta@public-inbox.org>
+# Filesystem-based critbit implementation in pure Perl based on
+# agl's take on djb's public domain code: git://github.com/agl/critbit.git
+# Provides bidirectional mapping between MIDs and stable article nums
+# See __END__ for layout details
+package PublicInbox::CBM;
+use fields qw(fd fh);
+use strict;
+use warnings;
+use Fcntl qw(:flock SEEK_SET SEEK_END SEEK_CUR O_RDWR O_RDONLY O_CREAT O_EXCL);
+BEGIN { require 'syscall.ph' };
+use constant MY_pread => (eval { &SYS_pread64 } || eval { &SYS_pread });
+use constant MY_pwrite => (eval { &SYS_pwrite64 } || eval { &SYS_pwrite });
+use Carp qw/confess/;
+
+use constant {
+	PTR_SIZE => 8,
+	CBM_PTR => 'Q<', # 5.10 feature
+	CBM_NUM => 'Q<',
+	ALIGN_SIZE => 16,
+};
+
+use constant {
+	ALIGN_MASK => ~(ALIGN_SIZE - 1),
+	offsetof_hdr_msgnum => PTR_SIZE * 4,
+	offsetof_hdr_msgid_root => PTR_SIZE * 5,
+	offsetof_hdr_msgnum_root => PTR_SIZE * 6,
+	offsetof_hdr_free_root => PTR_SIZE * 7,
+	CBM_NULL => pack(CBM_PTR, 0),
+	offsetof_rec_mid_capa => 44,
+	offsetof_rec_num => 48,
+	sizeof_msgnum => 8,
+	sizeof_record => 64, # + msgid flex array
+};
+
+sub new {
+	my ($class, $path, $writable) = @_;
+	my $m;
+
+	if (!defined($writable) || $writable == 0) { $m = O_RDONLY; }
+	elsif ($writable == 2) { $m = O_RDWR }
+	elsif ($writable == 1) { $m = O_RDWR | O_CREAT | O_EXCL }
+	else { die "writable: `$writable' not understood\n" }
+	my $self = fields::new($class);
+
+	sysopen $self->{fh}, $path, $m or die "open $path failed: $!\n";
+	$self->{fd} = fileno($self->{fh}) + 0;
+	if ($writable == 1) {
+		my $p = sysseek($self->{fh}, 0, SEEK_END) or die "seek: $!\n";
+		# initialize the header on creation
+		my $z = "\0";
+		xpwrite($self->{fd}, $z, 63) if ($p == 0);
+	}
+	$self;
+}
+
+# msgid must be clean without '<>'
+# Returns the new msgnum if inserted, undef if it exists
+sub cbm_insert {
+	my ($self, $msgid) = @_;
+	lockrw($self);
+	my $msgnum = eval { do_insert($self, $msgid) };
+	my $err = $@;
+	unlock($self);
+	die $err if $err;
+	$msgnum;
+}
+
+sub align { ($_[0] + ALIGN_SIZE - 1) & ALIGN_MASK }
+
+sub alloc_node {
+	my ($self) = @_;
+	my $end = sysseek($self->{fh}, 0, SEEK_END) or die "seek: $!\n";
+	align($end);
+}
+
+# returns a new msgnum
+sub do_insert {
+	my ($self, $ubytes) = @_; # ubytes is a Message-ID
+	my $fd = $self->{fd};
+	my $root = xpread($fd, PTR_SIZE, offsetof_hdr_msgid_root);
+	my $ulen = bytes::length($ubytes);
+
+	# empty tree, easy
+	if ($root eq CBM_NULL) {
+		my $n = next_msgnum($fd);
+		my $newnode = alloc_node($self);
+		my $buf = pack('L<Q<A*', $ulen, $n, $ubytes);
+		xpwrite($fd, $buf, $newnode + offsetof_rec_mid_capa);
+
+		# t->root = x;
+		xpwrite($fd, pack(CBM_PTR, $newnode), offsetof_hdr_msgid_root);
+		return $n;
+	}
+
+	my @ubytes = unpack('C*', $ubytes);
+	my $p = $root = unpack(CBM_PTR, $root);
+	while (1 & $p) {
+		my ($q0, $q1, $byte, $otherbits) = mid_node($fd, $p - 1);
+		my $c = ($byte < $ulen) ? $ubytes[$byte] : 0;
+		$p = ((1 + ($otherbits | $c)) >> 8) ? $q1 : $q0;
+	}
+
+	my $newotherbits;
+	my ($nearnum, $nearmid) = node_unpack($fd, $p);
+	my @p = unpack('C*', $nearmid);
+	my $pnew;
+	my $newbyte = 0;
+	while ($newbyte < $ulen) {
+		$pnew = $p[$newbyte];
+		my $unew = $ubytes[$newbyte];
+		if ($pnew != $unew) {
+			$newotherbits = $pnew ^ $unew;
+			goto different_byte_found;
+		}
+		++$newbyte;
+	}
+	$pnew = $p[$newbyte];
+	if ($pnew != 0) {
+		$newotherbits = $pnew;
+		goto different_byte_found;
+	}
+	return undef; # duplicate
+
+different_byte_found:
+	$newotherbits |= $newotherbits >> 1;
+	$newotherbits |= $newotherbits >> 2;
+	$newotherbits |= $newotherbits >> 4;
+	$newotherbits = ($newotherbits & ~($newotherbits >> 1)) ^ 255;
+	my $c = $pnew;
+	my $newdirection = (1 + ($newotherbits | $c)) >> 8;
+	my $wherep = offsetof_hdr_msgid_root;
+	$p = $root;
+	while ($p & 1) {
+		my $q = $p - 1;
+		my ($q0, $q1, $byte, $otherbits) = mid_node($fd, $q);
+		last if ($byte > $newbyte);
+		last if ($byte == $newbyte && $otherbits > $newotherbits);
+		my $c = $byte < $ulen ? $ubytes[$byte] : 0;
+		my $direction = (1 + ($otherbits | $c)) >> 8;
+		$wherep = $direction ? $q : $q + PTR_SIZE;
+		$p = unpack(CBM_PTR, xpread($fd, PTR_SIZE, $wherep));
+	}
+
+	my $n = next_msgnum($fd);
+
+	my $newnode = alloc_node($self);
+
+	my @child = $newdirection ? ($newnode, $p) : ($p, $newnode);
+	my $buf = pack('Q<Q<Q<Q<L<L<CCSL<Q<A*',
+			$child[0], $child[1], 0, 0,
+			$newbyte, 0,
+			$newotherbits, 0, 0,
+			$ulen, $n, $ubytes);
+
+	xpwrite($fd, $buf, $newnode);
+	xpwrite($fd, pack(CBM_PTR, $newnode + 1), $wherep);
+
+	$n;
+}
+
+sub msgnum_lookup {
+	my ($fd, $umsgid) = @_;
+	my $root = xpread($fd, PTR_SIZE, offsetof_hdr_msgid_root);
+	return undef if $root eq CBM_NULL;
+	my $p = unpack(CBM_PTR, $root);
+	my @ubytes = unpack('C*', $umsgid);
+	my $ulen = scalar @ubytes;
+	while (1 & $p) {
+		my ($q0, $q1, $byte, $otherbits) = mid_node($fd, $p - 1);
+		my $c = ($byte < $ulen) ? $ubytes[$byte] : 0;
+		$p = ((1 + ($otherbits | $c)) >> 8) ? $q1 : $q0;
+	}
+	my @x;
+	my ($msgnum, $msgid) = @x = node_unpack($fd, $p);
+	($msgid eq $umsgid) ? $msgnum : undef;
+}
+
+sub msgid_lookup {
+	my ($fd, $num) = @_;
+	my $root = xpread($fd, PTR_SIZE, offsetof_hdr_msgid_root);
+	return undef if $root eq CBM_NULL;
+	my $p = unpack(CBM_PTR, $root);
+	my $ubytes = pack(CBM_PTR, $num + 0);
+	my @ubytes = unpack('C*', $ubytes);
+	my $ulen = scalar @ubytes;
+	while (1 & $p) {
+		my ($q0, $q1, $byte, $otherbits) = num_node($fd, $p - 1);
+		my $c = ($byte < $ulen) ? $ubytes[$byte] : 0;
+		$p = ((1 + ($otherbits | $c)) >> 8) ? $q1 : $q0;
+	}
+	my ($msgnum, $msgid) = node_unpack($fd, $p);
+	($msgnum == $num) ? $msgid : undef;
+}
+
+sub mid_node {
+	my ($fd, $q) = @_;
+	my @u = unpack('Q<Q<Q<Q<L<L<C', xpread($fd, 41, $q));
+	($u[0], $u[1], $u[4], $u[6]);
+}
+
+sub num_node {
+	my ($fd, $q) = @_;
+	my @u = unpack('Q<Q<L<L<CC', xpread($fd, 26, $q + 16));
+	($u[0], $u[1], $u[3], $u[5]);
+}
+
+sub node_unpack {
+	my ($fd, $q) = @_;
+	# my $q = unpack(CBM_PTR, $p);
+	my $capa = xpread($fd, 4, $q + offsetof_rec_mid_capa);
+	$capa = unpack('L<', $capa);
+	my $buf = xpread($fd, sizeof_msgnum + $capa, $q + offsetof_rec_num);
+	unpack("Q<A$capa", $buf); # (uin64_t msgnum, uint8_t msgid[])
+};
+
+# lookup the clean Message-ID given a message number
+sub cbm_msgid {
+	my ($self, $msgnum) = @_;
+	lockro($self);
+	my $msgid = msgid_lookup($self->{fd}, $msgnum);
+	unlock($self);
+	$msgid;
+}
+
+# returns the message number given a clean Message-ID
+sub cbm_msgnum {
+	my ($self, $msgid) = @_;
+	lockro($self);
+	my $msgnum = eval { msgnum_lookup($self->{fd}, $msgid) };
+	my $err = $@;
+	unlock($self);
+	die $err if $err;
+	$msgnum;
+}
+
+sub cbm_delete {
+	my ($self, $msgid) = @_; # TODO
+}
+
+sub cbm_prefixed {
+	my ($self, $msgid_pfx, $max) = @_;
+}
+
+sub next_msgnum {
+	my ($fd) = @_;
+	my $buf = xpread($fd, sizeof_msgnum, offsetof_hdr_msgnum);
+	my $num = unpack(CBM_NUM, $buf) + 1;
+	xpwrite($fd, pack(CBM_NUM, $num), offsetof_hdr_msgnum);
+	$num;
+}
+
+sub xpread {
+	# my ($fd, $count, $offset) = @_;
+	my $count = $_[1] + 0;
+	my $buf = "\0" x $count;
+	# pread(fd, buf, count, offset)
+	my $nr = syscall(MY_pread, $_[0], $buf, $count, $_[2]);
+	return $buf if $nr == $count;
+	confess "pread failed: $!\n" if $nr < 0;
+	confess "pread partial (got:$nr < expected:$count)";
+}
+
+sub xpwrite {
+	# my ($fd, $buf, $offset) = @_;
+	use bytes;
+	my $count = bytes::length($_[1]);
+	# pwrite(fd, buf, count, offset)
+	my $nr = syscall(MY_pwrite, $_[0], $_[1], $count, $_[2]);
+	return $nr if ($count == $nr);
+
+	confess "pwrite failed: $!\n" if $nr < 0;
+	confess "pwrite partial (got:$nr < expected:$count)";
+}
+
+sub lockrw { flock($_[0]->{fh}, LOCK_EX) or die "flock(LOCK_EX): $!\n" }
+sub lockro { flock($_[0]->{fh}, LOCK_SH) or die "flock(LOCK_SH): $!\n" }
+sub unlock { flock($_[0]->{fh}, LOCK_UN) or die "flock(LOCK_UN): $!\n" }
+
+1;
+__END__
+This is superior to existing on-disk databases (which I know about)
+as it allows prefix matching of Message-IDs.  Prefix matching of
+Message-IDs allows us to implement partial lookups when URLs get
+truncated.
+
+Unsigned 64-bit (LE) file offsets are "pointers" in the file,
+
+/* This is what we store and care about in the database */
+struct rec {
+	uint64_t mid_child_0;
+	uint64_t mid_child_1;
+	uint64_t num_child_0;
+	uint64_t num_child_1;
+	/* ^^ 32 bytes ^^ */
+	uint32_t mid_byte;
+	uint32_t num_byte;
+	uint8_t mid_otherbits;
+	uint8_t num_otherbits;
+	uint16_t padding;
+	uint32_t id_capa; /* max: 998, ref rfc2822 2.2.1 */
+	/* ^^ 48 bytes ^^ */
+
+	/* the actual data we care about: */
+	uint64_t num;
+	uint8_t id[]; /* flex array */
+} __attribute__((packed));
+
+# Data structure documentation in C
+struct free_node {
+	uint64_t free_child_0;
+	uint64_t free_child_1;
+	uint32_t free_byte;
+	uint8_t free_otherbits;
+	uint32_t capa;
+	uint64_t head_ptr;
+};
+
+struct any {
+	union {
+		struct free_node;
+		struct rec;
+	} as;
+};
+
+struct file_header {
+	uint64_t reserved[4];
+	uint64_t msgnum;
+	struct rec *msgid_root;
+	struct rec *msgnum_root;
+	struct free_node *free_root;
+};
+
+Note: the '*' pointer notation denotes a 64-bit LE unsigned file offset,
+not a machine/process-dependent memory pointer.
+
+This document is dedicated to the Public domain (CC0):
+http://creativecommons.org/publicdomain/zero/1.0/
diff --git a/t/cbm.t b/t/cbm.t
new file mode 100644
index 0000000..3425920
--- /dev/null
+++ b/t/cbm.t
@@ -0,0 +1,25 @@
+# CC0 (Public domain): http://creativecommons.org/publicdomain/zero/1.0/
+# Note: the rest of public-inbox is AGPLv3 <meta@public-inbox.org>
+use strict;
+use warnings;
+use Test::More;
+use File::Temp qw/tempdir/;
+
+use_ok 'PublicInbox::CBM';
+my $tmpdir = tempdir(CLEANUP => 1);
+my $f = "$tmpdir/cbm";
+my $db = PublicInbox::CBM->new($f, 1);
+
+my $n = $db->cbm_insert("hello-world");
+is(1, $n, "first message inserted is 1");
+my $c = $db->cbm_insert("ablahello-world!");
+is(2, $c, "second message inserted is 2");
+foreach my $i (1..200) {
+	is($i + 2, $db->cbm_insert("msg".$i), "insert is sequential ($i)");
+}
+#print STDERR $db->cbm_msgid(2), "\n";
+is(1, $db->cbm_msgnum("hello-world"), "msgid lookup works");
+# system "cat $f | hexdump -C >&2";
+
+done_testing();
+1;
-- 
EW


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2015-09-06  3:03 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2015-09-06  3:03 [PATCH] work-in-progress: crit-bit map Eric Wong

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).