about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorKJ Tsanaktsidis <ktsanaktsidis@zendesk.com>2021-11-25 17:56:19 +1100
committerEric Wong <e@80x24.org>2021-11-26 18:17:54 +0000
commit18ee38af90b527f617af7bf9bbcf2e8e7cb0df17 (patch)
treeb1c92e9bd1913a330a63edfbcd5b36a4b12b0c7f
parent00fb3c8fc14c7f02cee27dba076501b30e739445 (diff)
downloadraindrops-18ee38af90b527f617af7bf9bbcf2e8e7cb0df17.tar.gz
Currently, all memory used by Raindrops is mapped as MAP_ANONYMOUS. This
means that although Raindrops counters can be shared between processes
that have forked from each other, it is not possible to share the
counter values with another, unrelated process.

This patch adds support for backing the Raindrops mapping with a file
descriptor obtained from an IO object. The #initialize API has been
enhanced with two new keyword options:

  Raindrops.new(size, io: nil, zero: false)

If an instance of IO is provided, then the underlying file descriptor
for that IO will be used to back the memory mapping Raindrops creates.
An unrelated process can then open the same file, and read the counters;
either by mmap'ing the file itself (or using Raindrops to do so), or by
making ordinary seek()/read() calls if performance is not a concern.

Note that the provided IO object _must_ implement #truncate; this is
used to set the size of the file to be right-sized for the memory
mapping that is created.

If the zero argument is passed as true, then the mapping will be zero'd
by Raindrops as part of its initialization. If it's false, then the
Raindrops counters existing in the file will be preserved. This allows
counter values to be persisted (although note that Raindrops makes no
attempt to msync the values, so they are not durable to e.g. system
crashes).

Counter values can easily be shared between processes
in-memory only without touching the disk by passing in a File on a
tmpfs as the io object.
-rw-r--r--ext/raindrops/raindrops.c63
-rw-r--r--lib/raindrops.rb24
-rw-r--r--test/test_raindrops.rb42
3 files changed, 114 insertions, 15 deletions
diff --git a/ext/raindrops/raindrops.c b/ext/raindrops/raindrops.c
index 837084c..72a6ee7 100644
--- a/ext/raindrops/raindrops.c
+++ b/ext/raindrops/raindrops.c
@@ -4,6 +4,7 @@
 #include <assert.h>
 #include <errno.h>
 #include <stddef.h>
+#include <string.h>
 #include "raindrops_atomic.h"
 
 #ifndef SIZET2NUM
@@ -34,10 +35,18 @@ struct raindrops {
         size_t size;
         size_t capa;
         pid_t pid;
+        VALUE io;
         struct raindrop *drops;
 };
 
 /* called by GC */
+static void rd_mark(void *ptr)
+{
+        struct raindrops *r = ptr;
+        rb_gc_mark(r->io);
+}
+
+/* called by GC */
 static void rd_free(void *ptr)
 {
         struct raindrops *r = ptr;
@@ -60,7 +69,7 @@ static size_t rd_memsize(const void *ptr)
 
 static const rb_data_type_t rd_type = {
         "raindrops",
-        { NULL, rd_free, rd_memsize, /* reserved */ },
+        { rd_mark, rd_free, rd_memsize, /* reserved */ },
         /* parent, data, [ flags ] */
 };
 
@@ -87,16 +96,10 @@ static struct raindrops *get(VALUE self)
 }
 
 /*
- * call-seq:
- *        Raindrops.new(size)        -> raindrops object
- *
- * Initializes a Raindrops object to hold +size+ counters.  +size+ is
- * only a hint and the actual number of counters the object has is
- * dependent on the CPU model, number of cores, and page size of
- * the machine.  The actual size of the object will always be equal
- * or greater than the specified +size+.
+ * This is the _actual_ implementation of #initialize - the Ruby wrapper
+ * handles keyword-argument handling then calls this method.
  */
-static VALUE init(VALUE self, VALUE size)
+static VALUE init_cimpl(VALUE self, VALUE size, VALUE io, VALUE zero)
 {
         struct raindrops *r = DATA_PTR(self);
         int tries = 1;
@@ -113,9 +116,19 @@ static VALUE init(VALUE self, VALUE size)
         r->capa = tmp / raindrop_size;
         assert(PAGE_ALIGN(raindrop_size * r->capa) == tmp && "not aligned");
 
+        r->io = io;
+
 retry:
-        r->drops = mmap(NULL, tmp,
-                        PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
+        if (RTEST(r->io)) {
+                int fd = NUM2INT(rb_funcall(r->io, rb_intern("fileno"), 0));
+                rb_funcall(r->io, rb_intern("truncate"), 1, SIZET2NUM(tmp));
+                r->drops = mmap(NULL, tmp,
+                                PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
+        } else {
+                r->drops = mmap(NULL, tmp,
+                                PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED,
+                                -1, 0);
+        }
         if (r->drops == MAP_FAILED) {
                 int err = errno;
 
@@ -127,6 +140,9 @@ retry:
         }
         r->pid = getpid();
 
+        if (RTEST(zero))
+                memset(r->drops, 0, tmp);
+
         return self;
 }
 
@@ -217,14 +233,16 @@ static VALUE capa(VALUE self)
  * call-seq:
  *        rd.dup                -> rd_copy
  *
- * Duplicates and snapshots the current state of a Raindrops object.
+ * Duplicates and snapshots the current state of a Raindrops object. Even
+ * if the given Raindrops object is backed by a file, the copy will be backed
+ * by independent, anonymously mapped memory.
  */
 static VALUE init_copy(VALUE dest, VALUE source)
 {
         struct raindrops *dst = DATA_PTR(dest);
         struct raindrops *src = get(source);
 
-        init(dest, SIZET2NUM(src->size));
+        init_cimpl(dest, SIZET2NUM(src->size), Qnil, Qfalse);
         memcpy(dst->drops, src->drops, raindrop_size * src->size);
 
         return dest;
@@ -375,6 +393,20 @@ static VALUE evaporate_bang(VALUE self)
         return Qnil;
 }
 
+/*
+ * call-seq:
+ *         to_io        -> IO
+ *
+ * Returns the IO object backing the memory for this raindrop, if
+ * one was specified when constructing this Raindrop. If this
+ * Raindrop is backed by anonymous memory, this method returns nil.
+ */
+static VALUE to_io(VALUE self)
+{
+        struct raindrops *r = get(self);
+        return r->io;
+}
+
 void Init_raindrops_ext(void)
 {
         VALUE cRaindrops = rb_define_class("Raindrops", rb_cObject);
@@ -433,7 +465,7 @@ void Init_raindrops_ext(void)
 
         rb_define_alloc_func(cRaindrops, alloc);
 
-        rb_define_method(cRaindrops, "initialize", init, 1);
+        rb_define_private_method(cRaindrops, "initialize_cimpl", init_cimpl, 3);
         rb_define_method(cRaindrops, "incr", incr, -1);
         rb_define_method(cRaindrops, "decr", decr, -1);
         rb_define_method(cRaindrops, "to_ary", to_ary, 0);
@@ -444,6 +476,7 @@ void Init_raindrops_ext(void)
         rb_define_method(cRaindrops, "capa", capa, 0);
         rb_define_method(cRaindrops, "initialize_copy", init_copy, 1);
         rb_define_method(cRaindrops, "evaporate!", evaporate_bang, 0);
+        rb_define_method(cRaindrops, "to_io", to_io, 0);
 
 #ifdef __linux__
         Init_raindrops_linux_inet_diag();
diff --git a/lib/raindrops.rb b/lib/raindrops.rb
index ba273eb..dc61952 100644
--- a/lib/raindrops.rb
+++ b/lib/raindrops.rb
@@ -36,6 +36,30 @@ class Raindrops
     def total
       active + queued
     end
+  end unless defined? ListenStats
+
+  # call-seq:
+  #        Raindrops.new(size, io: nil)        -> raindrops object
+  #
+  # Initializes a Raindrops object to hold +size+ counters.  +size+ is
+  # only a hint and the actual number of counters the object has is
+  # dependent on the CPU model, number of cores, and page size of
+  # the machine.  The actual size of the object will always be equal
+  # or greater than the specified +size+.
+  # If +io+ is provided, then the Raindrops memory will be backed by
+  # the specified file; otherwise, it will allocate anonymous memory.
+  # The IO object must respond to +truncate+, as this is used to set
+  # the size of the file.
+  # If +zero+ is provided, then the memory region is zeroed prior to
+  # returning. This is only meaningful if +io+ is also provided; in
+  # that case it controls whether any existing counter values in +io+
+  # are retained (false) or whether it is entirely zeroed (true).
+  def initialize(size, io: nil, zero: false)
+    # This ruby wrapper exists to handle the keyword-argument handling,
+    # which is otherwise kind of awkward in C. We delegate the keyword
+    # arguments to the _actual_ initialize implementation as positional
+    # args.
+    initialize_cimpl(size, io, zero)
   end
 
   autoload :Linux, 'raindrops/linux'
diff --git a/test/test_raindrops.rb b/test/test_raindrops.rb
index 0749694..6351c66 100644
--- a/test/test_raindrops.rb
+++ b/test/test_raindrops.rb
@@ -1,6 +1,7 @@
 # -*- encoding: binary -*-
 require 'test/unit'
 require 'raindrops'
+require 'tempfile'
 
 class TestRaindrops < Test::Unit::TestCase
 
@@ -162,4 +163,45 @@ class TestRaindrops < Test::Unit::TestCase
     assert status.success?
     assert_equal [ 1, 2 ], tmp.to_ary
   end
+
+  def test_io_backed
+    file = Tempfile.new('test_io_backed')
+    rd = Raindrops.new(4, io: file, zero: true)
+    rd[0] = 123
+    rd[1] = 456
+
+    assert_equal 123, rd[0]
+    assert_equal 456, rd[1]
+
+    rd.evaporate!
+
+    file.rewind
+    data = file.read
+    assert_equal 123, data.unpack('L!')[0]
+    assert_equal 456, data[Raindrops::SIZE..data.size].unpack('L!')[0]
+  end
+
+  def test_io_backed_reuse
+    file = Tempfile.new('test_io_backed')
+    rd = Raindrops.new(4, io: file, zero: true)
+    rd[0] = 123
+    rd[1] = 456
+    rd.evaporate!
+
+    rd = Raindrops.new(4, io: file, zero: false)
+    assert_equal 123, rd[0]
+    assert_equal 456, rd[1]
+  end
+
+  def test_iobacked_noreuse
+    file = Tempfile.new('test_io_backed')
+    rd = Raindrops.new(4, io: file, zero: true)
+    rd[0] = 123
+    rd[1] = 456
+    rd.evaporate!
+
+    rd = Raindrops.new(4, io: file, zero: true)
+    assert_equal 0, rd[0]
+    assert_equal 0, rd[1]
+  end
 end