From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on dcvr.yhbt.net X-Spam-Level: X-Spam-ASN: AS30031 170.10.128.0/23 X-Spam-Status: No, score=-4.6 required=3.0 tests=BAYES_00,DKIMWL_WL_HIGH, DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,RCVD_IN_DNSWL_LOW, RCVD_IN_MSPIKE_H4,RCVD_IN_MSPIKE_WL,SPF_HELO_NONE,SPF_PASS shortcircuit=no autolearn=ham autolearn_force=no version=3.4.2 Received: from us-smtp-delivery-110.mimecast.com (us-smtp-delivery-110.mimecast.com [170.10.129.110]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by dcvr.yhbt.net (Postfix) with ESMTPS id 03EC91F953 for ; Thu, 25 Nov 2021 06:57:53 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=zendesk.com; s=mimecast20150210; t=1637823472; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=EYDobeBAwsUNQUqbXLpzHHsjKma2KrdFcqlIIpHOOFg=; b=WGLv11jNdNACGnuUmuVFPP/D2spos+MDxIfWyKxaP17At94Z5rDhz75srWZd1Zc84GjKiv cZ3nE1XWzR7cReYHihTz4H7wucbSuNCES1lbNDCWSmebmavzPBrSGWEU82inZ7t2fjcvfa T5L45Uk2JL6VYRxjbHuiYr/H3GuxGk4= Received: from mail-pl1-f197.google.com (mail-pl1-f197.google.com [209.85.214.197]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id us-mta-105-noH9MMQ0OgeCsl3c-PwoWg-1; Thu, 25 Nov 2021 01:57:51 -0500 X-MC-Unique: noH9MMQ0OgeCsl3c-PwoWg-1 Received: by mail-pl1-f197.google.com with SMTP id p3-20020a170903248300b00143c00a5411so1664822plw.12 for ; Wed, 24 Nov 2021 22:57:51 -0800 (PST) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:mime-version :content-transfer-encoding; bh=Dzn+QYnRb4oAVMnfqyP4pU0Fo5/dp47l9J9jaeiF0XY=; b=eOQRd0/nO+OcMgdzM8/6ALeR4v5UHzkaOTzJfru4Z0kcdgi0o40Rwg7p58MJP6KjUF Vb+SajUYcmQP0zhiTKSEr4vZk1acpfe42hB6JakHjtVAV0eeKkwfAVJ3FYnvVVr6zY5D B9aX5pmhqMJIH9CjcSWU+nkBhvirlaLUX63AHMPImxqnNdbvrFJDWJ5wIS6Ksyh4WQAO VcJP0EM3xDOW4n2JpiE1KD4VnnTGahIILK9jMxJKqLiIhGdls3zYeGQeEe08TGYKVvI4 iHgqG8O1xxF38I9B/g85xS51IuvQI4qX3Gqu7F50BfpIDgV/hWbzZqLqDKdfzhcvia86 E2mg== X-Gm-Message-State: AOAM5323nLvPcXLPjcAB8ZjP16RuluFBTxjOHVA5V9mYBGQLE80Jui5I nahjw5tWEMJKemRy85Eo2Szcjv/5Qsz4+voLwVjjL1xVC8zzVbjn4Z9UwNALBjxgGo+RrJyBDCP rLHnNuk4Ca+e0nsB0ZtP4cKPykfShPTk9Lk13fNlRpT/lvZbmLOsr6dysGb+z40CjdqQrctmV83 P5lF4= X-Received: by 2002:a17:902:ab94:b0:143:beb5:b6a7 with SMTP id f20-20020a170902ab9400b00143beb5b6a7mr28101335plr.30.1637823469954; Wed, 24 Nov 2021 22:57:49 -0800 (PST) X-Google-Smtp-Source: ABdhPJynk9Rsb9Wn/i1vfbqCfavBDl51/hNEI8M6pP+Cy7vKjPkh93r50ZPaZHyOIsqdttwk/NoTyQ== X-Received: by 2002:a17:902:ab94:b0:143:beb5:b6a7 with SMTP id f20-20020a170902ab9400b00143beb5b6a7mr28101291plr.30.1637823469407; Wed, 24 Nov 2021 22:57:49 -0800 (PST) Received: from 8927-ktsanaktsidis.zdcorp.com (ec2-13-55-49-175.ap-southeast-2.compute.amazonaws.com. [13.55.49.175]) by smtp.googlemail.com with ESMTPSA id on6sm8008793pjb.47.2021.11.24.22.57.47 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Wed, 24 Nov 2021 22:57:49 -0800 (PST) From: KJ Tsanaktsidis To: raindrops-public@yhbt.net Cc: KJ Tsanaktsidis Subject: [PATCH v2] Allow Raindrops objects to be backed by a file Date: Thu, 25 Nov 2021 17:56:19 +1100 Message-Id: <20211125065618.3432-1-ktsanaktsidis@zendesk.com> X-Mailer: git-send-email 2.33.1 MIME-Version: 1.0 Authentication-Results: relay.mimecast.com; auth=pass smtp.auth=CUSA10A63 smtp.mailfrom=ktsanaktsidis@zendesk.com X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: zendesk.com Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="US-ASCII" List-Id: Thanks for your feedback on my previous patch Eric, I think backing Raindrops by an arbitrary file (which can actually be on tmpfs) is much neater than harcdoding some stuff in there for memfd. Looking forward to hearing your thoughts on this one! --------------------------------------- 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 theat 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). On Linux, 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. --- ext/raindrops/raindrops.c | 63 +++++++++++++++++++++++++++++---------- lib/raindrops.rb | 24 +++++++++++++++ test/test_raindrops.rb | 42 ++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 15 deletions(-) diff --git a/ext/raindrops/raindrops.c b/ext/raindrops/raindrops.c index 837084c..8cd9ed2 100644 --- a/ext/raindrops/raindrops.c +++ b/ext/raindrops/raindrops.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "raindrops_atomic.h" =20 #ifndef SIZET2NUM @@ -34,9 +35,17 @@ struct raindrops { =09size_t size; =09size_t capa; =09pid_t pid; +=09VALUE io; =09struct raindrop *drops; }; =20 +/* called by GC */ +static void rd_mark(void *ptr) +{ +=09struct raindrops *r =3D ptr; +=09rb_gc_mark(r->io); +} + /* called by GC */ static void rd_free(void *ptr) { @@ -60,7 +69,7 @@ static size_t rd_memsize(const void *ptr) =20 static const rb_data_type_t rd_type =3D { =09"raindrops", -=09{ NULL, rd_free, rd_memsize, /* reserved */ }, +=09{ rd_mark, rd_free, rd_memsize, /* reserved */ }, =09/* parent, data, [ flags ] */ }; =20 @@ -87,16 +96,10 @@ static struct raindrops *get(VALUE self) } =20 /* - * call-seq: - *=09Raindrops.new(size)=09-> 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 _acutal_ 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) { =09struct raindrops *r =3D DATA_PTR(self); =09int tries =3D 1; @@ -113,9 +116,18 @@ static VALUE init(VALUE self, VALUE size) =09r->capa =3D tmp / raindrop_size; =09assert(PAGE_ALIGN(raindrop_size * r->capa) =3D=3D tmp && "not aligned")= ; =20 +=09r->io =3D io; + retry: -=09r->drops =3D mmap(NULL, tmp, -=09 PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0); +=09if (RTEST(r->io)) { +=09=09rb_funcall(r->io, rb_intern("truncate"), 1, SIZET2NUM(tmp)); +=09=09int fd =3D NUM2INT(rb_funcall(r->io, rb_intern("fileno"), 0)); +=09=09r->drops =3D mmap(NULL, tmp, +=09=09=09=09PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); +=09} else { +=09=09r->drops =3D mmap(NULL, tmp, +=09=09=09=09PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0); +=09} =09if (r->drops =3D=3D MAP_FAILED) { =09=09int err =3D errno; =20 @@ -127,6 +139,10 @@ retry: =09} =09r->pid =3D getpid(); =20 +=09if (RTEST(zero)) { +=09=09memset(r->drops, 0, tmp); +=09} + =09return self; } =20 @@ -217,14 +233,16 @@ static VALUE capa(VALUE self) * call-seq: *=09rd.dup=09=09-> 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 bac= ked + * by independent, anonymously mapped memory. */ static VALUE init_copy(VALUE dest, VALUE source) { =09struct raindrops *dst =3D DATA_PTR(dest); =09struct raindrops *src =3D get(source); =20 -=09init(dest, SIZET2NUM(src->size)); +=09init_cimpl(dest, SIZET2NUM(src->size), Qnil, Qfalse); =09memcpy(dst->drops, src->drops, raindrop_size * src->size); =20 =09return dest; @@ -375,6 +393,20 @@ static VALUE evaporate_bang(VALUE self) =09return Qnil; } =20 +/* + * call-seq: + * =09to_io=09-> 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) +{ +=09struct raindrops *r =3D get(self); +=09return r->io; +} + void Init_raindrops_ext(void) { =09VALUE cRaindrops =3D rb_define_class("Raindrops", rb_cObject); @@ -433,7 +465,7 @@ void Init_raindrops_ext(void) =20 =09rb_define_alloc_func(cRaindrops, alloc); =20 -=09rb_define_method(cRaindrops, "initialize", init, 1); +=09rb_define_private_method(cRaindrops, "initialize_cimpl", init_cimpl, 3)= ; =09rb_define_method(cRaindrops, "incr", incr, -1); =09rb_define_method(cRaindrops, "decr", decr, -1); =09rb_define_method(cRaindrops, "to_ary", to_ary, 0); @@ -444,6 +476,7 @@ void Init_raindrops_ext(void) =09rb_define_method(cRaindrops, "capa", capa, 0); =09rb_define_method(cRaindrops, "initialize_copy", init_copy, 1); =09rb_define_method(cRaindrops, "evaporate!", evaporate_bang, 0); +=09rb_define_method(cRaindrops, "to_io", to_io, 0); =20 #ifdef __linux__ =09Init_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 ListenStats < Struct.new(:active, :queued) def total active + queued end + end unless defined? ListenStats + + # call-seq: + #=09Raindrops.new(size, io: nil)=09-> 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 =20 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' =20 class TestRaindrops < Test::Unit::TestCase =20 @@ -162,4 +163,45 @@ def test_evaporate_with_fork assert status.success? assert_equal [ 1, 2 ], tmp.to_ary end + + def test_io_backed + file =3D Tempfile.new('test_io_backed') + rd =3D Raindrops.new(4, io: file, zero: true) + rd[0] =3D 123 + rd[1] =3D 456 + + assert_equal 123, rd[0] + assert_equal 456, rd[1] + + rd.evaporate! + + file.rewind + data =3D 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 =3D Tempfile.new('test_io_backed') + rd =3D Raindrops.new(4, io: file, zero: true) + rd[0] =3D 123 + rd[1] =3D 456 + rd.evaporate! + + rd =3D Raindrops.new(4, io: file, zero: false) + assert_equal 123, rd[0] + assert_equal 456, rd[1] + end + + def test_iobacked_noreuse + file =3D Tempfile.new('test_io_backed') + rd =3D Raindrops.new(4, io: file, zero: true) + rd[0] =3D 123 + rd[1] =3D 456 + rd.evaporate! + + rd =3D Raindrops.new(4, io: file, zero: true) + assert_equal 0, rd[0] + assert_equal 0, rd[1] + end end --=20 2.33.1