MANIFEST | 2 ++
lib/PublicInbox/NDC_PP.pm | 29 +++++++++++++++++++++++++++++
lib/PublicInbox/Over.pm | 5 +++++
lib/PublicInbox/SearchIdx.pm | 5 ++++-
lib/PublicInbox/Spawn.pm | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++---
t/nodatacow.t | 34 ++++++++++++++++++++++++++++++++++
diff --git a/MANIFEST b/MANIFEST
index f46a0776ddbcd9901365cabf33fa68f4bfc8531e..d312e305cc0149ee1bcbe8bd5a34a13891c582ea 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -156,6 +156,7 @@ lib/PublicInbox/MboxGz.pm
lib/PublicInbox/MsgIter.pm
lib/PublicInbox/MsgTime.pm
lib/PublicInbox/Msgmap.pm
+lib/PublicInbox/NDC_PP.pm
lib/PublicInbox/NNTP.pm
lib/PublicInbox/NNTPD.pm
lib/PublicInbox/NNTPdeflate.pm
@@ -309,6 +310,7 @@ t/multi-mid.t
t/nntp.t
t/nntpd-tls.t
t/nntpd.t
+t/nodatacow.t
t/nulsubject.t
t/over.t
t/plack-2-txt-bodies.eml
diff --git a/lib/PublicInbox/NDC_PP.pm b/lib/PublicInbox/NDC_PP.pm
new file mode 100644
index 0000000000000000000000000000000000000000..0d20030d512903896dd2266fb73e2c8a4bfd369a
--- /dev/null
+++ b/lib/PublicInbox/NDC_PP.pm
@@ -0,0 +1,29 @@
+# Copyright (C) 2020 all contributors
+# License: AGPL-3.0+
+
+# Pure-perl class for Linux non-Inline::C users to disable COW for btrfs
+package PublicInbox::NDC_PP;
+use strict;
+use v5.10.1;
+
+sub set_nodatacow ($) {
+ my ($fd) = @_;
+ return if $^O ne 'linux';
+ defined(my $path = readlink("/proc/self/fd/$fd")) or return;
+ open my $mh, '<', '/proc/self/mounts' or return;
+ for (grep(/ btrfs /, <$mh>)) {
+ my (undef, $mnt_path, $type) = split(/ /);
+ next if $type ne 'btrfs'; # in case of false-positive from grep
+
+ # weird chars are escaped as octal
+ $mnt_path =~ s/\\(0[0-9]{2})/chr(oct($1))/egs;
+ $mnt_path .= '/' unless $mnt_path =~ m!/\z!;
+ if (index($path, $mnt_path) == 0) {
+ # error goes to stderr, but non-fatal for us
+ system('chattr', '+C', $path);
+ last;
+ }
+ }
+}
+
+1;
diff --git a/lib/PublicInbox/Over.pm b/lib/PublicInbox/Over.pm
index f32743c05a8a461baf0c4f5a52c983d5fecd25af..0146414cf4f2ebcf9575714b090e98992b201797 100644
--- a/lib/PublicInbox/Over.pm
+++ b/lib/PublicInbox/Over.pm
@@ -18,7 +18,12 @@ my ($self, $rw) = @_;
my $f = delete $self->{filename};
if (!-f $f) { # SQLite defaults mode to 0644, we want 0666
if ($rw) {
+ require PublicInbox::Spawn;
open my $fh, '+>>', $f or die "failed to open $f: $!";
+ PublicInbox::Spawn::set_nodatacow(fileno($fh));
+ my $j = "$f-journal";
+ open $fh, '+>>', $j or die "failed to open $j: $!";
+ PublicInbox::Spawn::set_nodatacow(fileno($fh));
} else {
$self->{filename} = $f; # die on stat() below:
}
diff --git a/lib/PublicInbox/SearchIdx.pm b/lib/PublicInbox/SearchIdx.pm
index 1fc574106c543a81d5fc4eef190746948a44e929..aa8d8ce325cf203d1ea77abc9e89d7bb5b73091b 100644
--- a/lib/PublicInbox/SearchIdx.pm
+++ b/lib/PublicInbox/SearchIdx.pm
@@ -125,8 +125,11 @@ $self->lock_acquire;
# don't create empty Xapian directories if we don't need Xapian
my $is_shard = defined($self->{shard});
- if (!$is_shard || ($is_shard && need_xapian($self))) {
+ if (!-d $dir && (!$is_shard ||
+ ($is_shard && need_xapian($self)))) {
File::Path::mkpath($dir);
+ opendir my $dh, $dir or die "opendir($dir): $!\n";
+ PublicInbox::Spawn::set_nodatacow(fileno($dh));
}
}
return unless defined $flag;
diff --git a/lib/PublicInbox/Spawn.pm b/lib/PublicInbox/Spawn.pm
index db679b770cfd81d985c4416db9b7186d718587a4..50f3185156a3d9160edc002508ee0e874997a3d0 100644
--- a/lib/PublicInbox/Spawn.pm
+++ b/lib/PublicInbox/Spawn.pm
@@ -10,6 +10,9 @@ # Currently, we only use this for code intended for long running
# daemons (inside the PSGI code (-httpd) and -nntpd). The short-lived
# scripts (-mda, -index, -learn, -init) either use IPC::run or standard
# Perl routines.
+#
+# There'll probably be more OS-level C stuff here, down the line.
+# We don't want too many DSOs: https://udrepper.livejournal.com/8790.html
package PublicInbox::Spawn;
use strict;
@@ -25,6 +28,7 @@ #include
#include
#include
#include
+#include
/* some platforms need alloca.h, but some don't */
#if defined(__GNUC__) && !defined(alloca)
@@ -144,12 +148,51 @@ return (int)pid;
}
VFORK_SPAWN
+# btrfs on Linux is copy-on-write (COW) by default. As of Linux 5.7,
+# this still leads to fragmentation for SQLite and Xapian files where
+# random I/O happens, so we disable COW just for SQLite files and Xapian
+# directories. Disabling COW disables checksumming, so we only do this
+# for regeneratable files, and not canonical git storage (git doesn't
+# checksum refs, only data under $GIT_DIR/objects).
+my $set_nodatacow = $^O eq 'linux' ? <<'SET_NODATACOW' : '';
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+void set_nodatacow(int fd)
+{
+ struct statfs buf;
+ int val = 0;
+
+ if (fstatfs(fd, &buf) < 0) {
+ fprintf(stderr, "fstatfs: %s\\n", strerror(errno));
+ return;
+ }
+
+ /* only btrfs is known to have this problem, so skip for non-btrfs */
+ if (buf.f_type != BTRFS_SUPER_MAGIC)
+ return;
+
+ if (ioctl(fd, FS_IOC_GETFLAGS, &val) < 0) {
+ fprintf(stderr, "FS_IOC_GET_FLAGS: %s\\n", strerror(errno));
+ return;
+ }
+ val |= FS_NOCOW_FL;
+ if (ioctl(fd, FS_IOC_SETFLAGS, &val) < 0)
+ fprintf(stderr, "FS_IOC_SET_FLAGS: %s\\n", strerror(errno));
+}
+SET_NODATACOW
+
my $inline_dir = $ENV{PERL_INLINE_DIRECTORY} //= (
$ENV{XDG_CACHE_HOME} //
( ($ENV{HOME} // '/nonexistent').'/.cache' )
).'/public-inbox/inline-c';
-$vfork_spawn = undef unless -d $inline_dir && -w _;
+$set_nodatacow = $vfork_spawn = undef unless -d $inline_dir && -w _;
if (defined $vfork_spawn) {
# Inline 0.64 or later has locking in multi-process env,
# but we support 0.5 on Debian wheezy
@@ -158,14 +201,21 @@ eval {
my $f = "$inline_dir/.public-inbox.lock";
open my $fh, '>', $f or die "failed to open $f: $!\n";
flock($fh, LOCK_EX) or die "LOCK_EX failed on $f: $!\n";
- eval 'use Inline C => $vfork_spawn';
+ eval 'use Inline C => $vfork_spawn . $set_nodatacow';
my $err = $@;
+ my $ndc_err;
+ if ($err && $set_nodatacow) { # missing Linux kernel headers
+ $ndc_err = $err;
+ undef $set_nodatacow;
+ eval 'use Inline C => $vfork_spawn';
+ }
flock($fh, LOCK_UN) or die "LOCK_UN failed on $f: $!\n";
die $err if $err;
+ warn $ndc_err if $ndc_err;
};
if ($@) {
warn "Inline::C failed for vfork: $@\n";
- $vfork_spawn = undef;
+ $set_nodatacow = $vfork_spawn = undef;
}
}
@@ -173,6 +223,13 @@ unless (defined $vfork_spawn) {
require PublicInbox::SpawnPP;
*pi_fork_exec = \&PublicInbox::SpawnPP::pi_fork_exec
}
+unless ($set_nodatacow) {
+ require PublicInbox::NDC_PP;
+ no warnings 'once';
+ *set_nodatacow = \&PublicInbox::NDC_PP::set_nodatacow;
+}
+undef $set_nodatacow;
+undef $vfork_spawn;
sub which ($) {
my ($file) = @_;
diff --git a/t/nodatacow.t b/t/nodatacow.t
new file mode 100644
index 0000000000000000000000000000000000000000..87b6bdf72142a72a008e3366f301a1e30a0a517f
--- /dev/null
+++ b/t/nodatacow.t
@@ -0,0 +1,34 @@
+#!perl -w
+# Copyright (C) 2020 all contributors
+# License: AGPL-3.0+
+use strict;
+use Test::More;
+use File::Temp qw(tempfile);
+use PublicInbox::TestCommon;
+use PublicInbox::Spawn qw(which);
+use_ok 'PublicInbox::NDC_PP';
+
+SKIP: {
+ my $nr = 2;
+ skip 'test is Linux-only', $nr if $^O ne 'linux';
+ my $dir = $ENV{BTRFS_TESTDIR};
+ skip 'BTRFS_TESTDIR not defined', $nr unless defined $dir;
+ skip 'chattr(1) not installed', $nr unless which('chattr');
+ my $lsattr = which('lsattr') or skip 'lsattr(1) not installed', $nr;
+ my ($fh, $name) = tempfile(DIR => $dir, UNLINK => 1);
+ BAIL_OUT "tempfile: $!" unless $fh && defined($name);
+ my $pp_sub = \&PublicInbox::NDC_PP::set_nodatacow;
+ $pp_sub->(fileno($fh));
+ my $res = xqx([$lsattr, $name]);
+ like($res, qr/C/, "`C' attribute set with pure Perl");
+
+ my $ic_sub = \&PublicInbox::Spawn::set_nodatacow;
+ $pp_sub == $ic_sub and
+ skip 'Inline::C or Linux kernel headers missing', 1;
+ ($fh, $name) = tempfile(DIR => $dir, UNLINK => 1);
+ $ic_sub->(fileno($fh));
+ $res = xqx([$lsattr, $name]);
+ like($res, qr/C/, "`C' attribute set with Inline::C");
+};
+
+done_testing;