{"id":16002727,"url":"https://github.com/dazuma/ractor-wrapper","last_synced_at":"2026-04-02T18:55:03.706Z","repository":{"id":54155439,"uuid":"343684514","full_name":"dazuma/ractor-wrapper","owner":"dazuma","description":"An experimental Ruby class that wraps a non-shareable object in a Ractor so it can be accessed by multiple Ractors concurrently.","archived":false,"fork":false,"pushed_at":"2021-03-12T20:25:16.000Z","size":28,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-04-27T02:25:57.998Z","etag":null,"topics":["ractor","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dazuma.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-03-02T07:32:39.000Z","updated_at":"2023-09-09T12:31:07.000Z","dependencies_parsed_at":"2022-08-13T07:50:30.209Z","dependency_job_id":null,"html_url":"https://github.com/dazuma/ractor-wrapper","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dazuma%2Fractor-wrapper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dazuma%2Fractor-wrapper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dazuma%2Fractor-wrapper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dazuma%2Fractor-wrapper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dazuma","download_url":"https://codeload.github.com/dazuma/ractor-wrapper/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243878439,"owners_count":20362432,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ractor","ruby"],"created_at":"2024-10-08T10:03:22.869Z","updated_at":"2026-04-02T18:55:03.694Z","avatar_url":"https://github.com/dazuma.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ractor::Wrapper\n\nRactor::Wrapper is an experimental class that wraps a non-shareable object in\nan actor, allowing multiple Ractors to access it concurrently.\n\n**WARNING:** This is an experimental library, and currently _not_ recommended\nfor production use. (As of Ruby 4.0, the same can still be said of Ractors in\ngeneral.)\n\n## Quick start\n\nInstall ractor-wrapper as a gem, or include it in your bundle.\n\n```sh\ngem install ractor-wrapper\n```\n\nRequire it in your code:\n\n```ruby\nrequire \"ractor/wrapper\"\n```\n\nYou can then create wrappers for objects. See the example below.\n\nRactor::Wrapper requires Ruby 4.0.0 or later.\n\n## What is Ractor::Wrapper?\n\nFor the most part, unless an object is _shareable_, which generally means\ndeeply immutable along with a few other restrictions, it cannot be accessed\ndirectly from a Ractor other than the one in which it was constructed. This\nmakes it difficult for multiple Ractors to share a resource that is stateful,\nsuch as a database connection.\n\n    +----Main-Ractor----+       +-Another-Ractor-+\n    |                   |       |                |\n    |      client1      |       |                |\n    |         |         |       |                |\n    |         | ok      |       |                |\n    |         v         |       |                |\n    |     my_db_conn \u003c------X------ client2      |\n    |                   | fails |                |\n    +-------------------+       +----------------+\n\nRactor::Wrapper makes it possible for an ordinary non-shareable object to\nbe accessed from multiple Ractors. It does this by \"wrapping\" the object with\na shareable proxy.\n\n    +--Main-Ractor--+    +-Wrapper-Ractor-+    +-Another-Ractor-+\n    |               |    |                |    |                |\n    |    client1    |    |                |    |    client2     |\n    |       |       |    |                |    |       |        |\n    |       v       |    |                |    |       v        |\n    |     +----------------------------------------------+      |\n    |     |             SHAREABLE    WRAPPER             |      |\n    |     +----------------------------------------------+      |\n    |               |    |        |       |    |                |\n    |               |    |        v       |    |                |\n    |               |    |   my_db_conn   |    |                |\n    +---------------+    +----------------+    +----------------+\n\nThe wrapper provides a shareable stub object that reproduces the method\ninterface of the original object, so, with a few caveats, the wrapper is almost\nfully transparent. Behind the scenes, the wrapper \"runs\" the wrapped object in\na controlled single-Ractor environment, and uses port messaging to communicate\nmethod calls, arguments, and return values between Ractors.\n\nRactor::Wrapper can be used to adapt non-shareable objects to a multi-Ractor\nworld. It can also be used to implement a simple actor by writing a \"plain\"\nRuby object and wrapping it with a Ractor.\n\n## Examples\n\nBelow are some illustrative examples showing how to use Ractor::Wrapper.\n\n### Net::HTTP example\n\nThe following example shows how to share a single Net::HTTP session object\namong multiple Ractors.\n\n```ruby\n# Net::HTTP example\n\nrequire \"ractor/wrapper\"\nrequire \"net/http\"\n\n# Create a Net::HTTP session. Net::HTTP sessions are not shareable,\n# so normally only one Ractor can access them at a time.\nhttp = Net::HTTP.new(\"example.com\")\nhttp.start\n\n# Create a wrapper around the session. This moves the session into an\n# internal Ractor and listens for method call requests. By default, a\n# wrapper serializes calls, handling one at a time, for compatibility\n# with non-thread-safe objects.\nwrapper = Ractor::Wrapper.new(http)\n\n# At this point, the session object can no longer be accessed directly\n# because it is now owned by the wrapper's internal Ractor.\n#     http.get(\"/whoops\")  # \u003c= raises Ractor::MovedError\n\n# However, you can access the session via the stub object provided by\n# the wrapper. This stub proxies the call to the wrapper's internal\n# Ractor. And it's shareable, so any number of Ractors can use it.\nresponse = wrapper.stub.get(\"/\")\n\n# Here, we start two Ractors, and pass the stub to each one. Each\n# Ractor can simply call methods on the stub as if it were the original\n# connection object. Internally, of course, the calls are proxied to\n# the original object via the wrapper, and execution is serialized.\nr1 = Ractor.new(wrapper.stub) do |stub|\n  5.times do\n    stub.get(\"/hello\")\n  end\n  :ok\nend\nr2 = Ractor.new(wrapper.stub) do |stub|\n  5.times do\n    stub.get(\"/ruby\")\n  end\n  :ok\nend\n\n# Wait for the two above Ractors to finish.\nr1.join\nr2.join\n\n# After you stop the wrapper, you can retrieve the underlying session\n# object and access it directly again.\nwrapper.async_stop\nhttp = wrapper.recover_object\nhttp.finish\n```\n\n### SQLite3 example\n\nThe following example shows how to share a SQLite3 database among multiple\nRactors.\n\n```ruby\n# SQLite3 example\n\nrequire \"ractor/wrapper\"\nrequire \"sqlite3\"\n\n# Create a SQLite3 database. These objects are not shareable, so\n# normally only one Ractor can access them.\ndb = SQLite3::Database.new($my_database_path)\n\n# Create a wrapper around the database. A SQLite3::Database object\n# cannot be moved between Ractors, so we configure the wrapper to run\n# in the current Ractor instead of an internal Ractor. We can also\n# configure it to run multiple worker threads because the database\n# object itself is thread-safe.\nwrapper = Ractor::Wrapper.new(db, use_current_ractor: true, threads: 2)\n\n# At this point, the database object can still be accessed directly\n# from the current Ractor because it hasn't been moved.\nrows = db.execute(\"select * from numbers\")\n\n# You can also access the database via the stub object provided by the\n# wrapper.\nrows = wrapper.stub.execute(\"select * from numbers\")\n\n# Here, we start two Ractors, and pass the stub to each one. The\n# wrapper's worker threads will handle the requests concurrently.\nr1 = Ractor.new(wrapper.stub) do |stub|\n  5.times do\n    stub.execute(\"select * from numbers\")\n  end\n  :ok\nend\nr2 = Ractor.new(wrapper.stub) do |stub|\n  5.times do\n    stub.execute(\"select * from numbers\")\n  end\n  :ok\nend\n\n# Wait for the two above Ractors to finish.\nr1.join\nr2.join\n\n# After stopping the wrapper, you can call the join method to wait for\n# it to completely finish.\nwrapper.async_stop\nwrapper.join\n\n# When running a wrapper with :use_current_ractor, you do not need to\n# recover the object, because it was never moved. The recover_object\n# method is not available.\n#     db2 = wrapper.recover_object  # \u003c= raises Ractor::Wrapper::Error\n```\n\n### Simple actor example\n\nThe following example demonstrates how to use Ractor::Wrapper to implement an\nactor as a plain Ruby object. Focus on writing functionality as methods, and\nlet Ractor::Wrapper handle all the messaging logic.\n\n```ruby\n# Simple actor example\n\nrequire \"ractor/wrapper\"\n\nclass SimpleCalculator\n  class EmptyStackError \u003c StandardError\n  end\n\n  def initialize\n    @stack = []\n  end\n\n  def push(number)\n    @stack.push(number)\n    nil\n  end\n\n  def pop\n    raise EmptyStackError if @stack.empty?\n    @stack.pop\n  end\n\n  def add\n    push(pop + pop)\n    nil\n  end\nend\n\n# Create an actor based on SimpleCalculator\ncalc_actor = Ractor::Wrapper.new(SimpleCalculator.new)\n\n# You can now send messages by calling methods\ncalc_stub = calc_actor.stub\ncalc_stub.push(2)\ncalc_stub.push(3)\ncalc_stub.add\nsum = calc_stub.pop\n\n# Stop the actor by calling async_stop\ncalc_actor.async_stop\n# Wait for the actor to shut down\ncalc_actor.join\n```\n\n## Configuring a wrapper\n\nRactor::Wrapper supports a fair amount of configuration, which may be needed in\norder to ensure good behavior of the wrapped object. You can configure many\naspects of Ractor::Wrapper by passing keyword arguments to its constructor.\nAlternatively, you can pass a block to the constructor; the constructor will\nyield a configuration interface to your block, letting you configure the\nwrapper's behavior in detail.\n\nThe various configuration options are described below.\n\n### Current Ractor mode\n\nNormally a wrapper will spawn a new Ractor and move the wrapped object into\nthat Ractor. We call this default mode the \"isolated Ractor\" mode. Isolated\nRactor lets the object function as an actor that can be called uniformly from\nany Ractor.\n\nHowever, some objects cannot be moved to a different Ractor. This in particular\ncan include certain C-based I/O objects such as database connections.\nAdditionally, there are other objects that can live only in the main Ractor. If\nthe object to be wrapped cannot be moved to its own Ractor, configure it with\n`use_current_ractor`, which will run the wrapper in a Thread in the calling\nRactor rather than trying to move it to its own Ractor. The SQLite3 example\nabove demonstrates wrapping an object that cannot be moved to its own Ractor.\n\n### Sequential vs concurrent execution\n\nBy default, wrappers run sequentially in a single Thread. The wrapper will\nhandle only a single method call at a time, and any other concurrent requests\nare queued and blocked until their turn. This is the behavior of the classic\nactor model, and in particular is appropriate for wrapped objects that are not\nthread-safe.\n\nYou can, however, configure a wrapper with concurrent access. This will spin up\na configurable number of worker threads within the wrapper, to handle\npotentially concurrent method calls. You should set this configuration only if\nyou are certain the wrapped object can handle concurrent access.\n\n### Data communication options\n\nWhen you call a method on a wrapper, and you pass arguments and receive a\nreturn value, or you pass a block that can receive arguments and return a\nvalue, those objects are communicated to and from the wrapper via Ractor ports.\nAs such, if they are not shareable, they may be *copied* or *moved*. By\ndefault, values are copied in order to minimize interference with surrounding\ncode, but a wrapper can be configured to move objects instead.\n\nThis configuration is done per-method, using the `configure_method` call in the\nconfiguration block. You can, for particular method names, specify whether each\ntype of value: arguments, return values, block arguments, and block return\nvalues, are copied or moved. For any given method, you must configure all\narguments to be handled the same way, but different methods can have different\nconfigurations. You can also provide a default configuration that will apply to\nall method names that are not explicitly configured.\n\nReturn values (and block return values) have a third configuration option:\n*void*. This option disables communication of return values, sending `nil`\ninstead of what was actually returned from the method. This is intended for\nmethods that do not *semantically* need to return anything, but because of\ntheir implementation they actually do return some internal object. You can use\nthe *void* option to prevent those methods from wasting resources copying a\nreturn object unnecessarily, or worse, moving an object that shouldn't be moved.\n\n### Block execution environment\n\nIf a block is passed to a method, it is handled in one of two ways. By default,\nif/when the method yields to the block, the wrapper will send a message *back*\nto the caller, and the block will be executed in the caller's environment. In\nmost cases, this is what you want; your block may access information from its\nlexical environment, and that environment would not be available to the wrapped\nobject. However, this extra communication can add overhead.\n\nAs an alternative, you can configure, per-method, blocks to be executed in the\ncontext of the *wrapped object*. Effectively, the block itself is *moved* into\nthe wrapped object's Ractor/context, and called directly. This will work only\nif the block does not access any information from its lexical context, or\nanything that cannot be accessed from a different Ractor. A block must truly be\nself-contained in order to use this option.\n\nAs with data communication options, configuring block execution environment is\ndone using the `configure_method` call in the configuration block. You can set\nthe environment either to `:caller` or `:wrapped`, and you can do so for an\nindividual method or provide a default to apply to all methods not explicitly\nconfigured.\n\n## Additional features\n\n### Wrapper shutdown\n\nIf you are done with a wrapper, you should shut it down by calling `async_stop`.\nThis method will initiate a graceful shutdown of the wrapper, finishing any\npending method calls, and putting the wrapper in a state where it will refuse\nnew calls. Any additional method calls will cause a\n`Ractor::Wrapper::StoppedError` to be raised.\n\nRactor::Wrapper also provides a `join` method that can be called to wait for\nthe wrapper to complete its shutdown.\n\n### Wrapped object access\n\nThe general intent is that once you've wrapped an object, all access should go\nthrough the wrapper. In the default \"isolated Ractor\" mode, the wrapped object\nis in fact *moved* to a different Ractor, so the Ractor system will prevent you\nfrom accessing it directly. In \"current Ractor\" mode, the wrapped object is not\nmoved, so you technically could continue to access it directly from its\noriginal Ractor. But beware: the wrapper runs a thread and will be making calls\nto the object from that thread, which may cause you problems if the object is\nnot thread-safe.\n\nIn \"isolated Ractor\" mode, after you shut down the wrapper, you can recover the\noriginal object by calling `recover_object`. Only one Ractor can call this\nmethod; the object will be moved into the requesting Ractor, and any other\nRactor that subsequently requests the object will get an exception instead.\n\nIn \"current Ractor\" mode, the object will never have been moved to a different\nRactor, so any pre-existing references (in the original Ractor) will still be\nvalid. In this case, `recover_object` is not necessary and will not be\navailable at all.\n\n### Error handling\n\nRactor::Wrapper provides fairly robust handling of errors. If a method call\nraises an exception, the exception will be passed back to the caller and raised\nthere. In the unlikely event that the wrapper itself crashes, it goes through a\nvery thorough clean-up process and makes every effort to shut down gracefully,\nnotifying any pending method calls that the wrapper has crashed by raising\n`Ractor::Wrapper::CrashedError`.\n\n### Automatic stub conversion\n\nOne special case handled by the wrapper is methods that return `self`. This is\na common pattern in Ruby and is used to allow \"chaining\" interfaces. However,\nyou generally cannot return `self` from a wrapped object because, depending on\nthe communication configuration, you'll either get a *copy* of `self`, or\nyou'll *move* the object out of the wrapper, thus breaking the wrapper. Thus,\nRactor::Wrapper explicitly detects when methods return `self`, and instead\nreplaces it with the wrapper's stub object. The stub is shareable, and designed\nto have the same usage as the original object, so this should work for most use\ncases.\n\n## Known issues\n\nRactors are in general somewhat \"bolted-on\" to Ruby, and there are a lot of\ncaveats to their use. This also applies to Ractor::Wrapper, which itself is\nessentially a workaround to the fact that Ruby has a lot of use cases that\nsimply don't play well in a Ractor world. Here we'll discuss some of the\ncaveats and known issues with Ractor::Wrapper.\n\n### Data communication issues\n\nAs of Ruby 4.0, most objects have been retrofitted to play reasonably with\nRactors. Some objects are shareable across Ractors, and most others can be\nmoved from one Ractor to another. However, there are a few objects that,\nbecause of their semantics or details about their implementation, cannot be\nmoved and are confined to their creating Ractor (or in some cases, only the\nmain Ractor.) These may include objects such as threads, procs, backtraces, and\ncertain C-based objects.\n\nOne particular case of note is exception objects, which one might expect to be\nshareable, but are not. Furthermore, they cannot be moved, and even copying an\nexception has issues (in particular the backtrace of a copy gets cleared out).\nSee https://bugs.ruby-lang.org/issues/21818 for more info. When a method raises\nan exception, Ractor::Wrapper communicates that exception via copying, which\nmeans that currently backtraces will not be present.\n\n### Blocks\n\nRuby blocks pose particular challenges for Ractor::Wrapper because of their\nsemantics and some of their common usage patterns. We've already seen above\nthat Ractor::Wrapper can run them either in the caller's context or in the\nwrapped object's context, which may limit what the block can do. Additionally,\nthe following restrictions apply to blocks:\n\nBlocks configured to run in the caller's context can be run only while the\nmethod is executing; i.e. they can only be \"yielded\" to. The wrapped object\ncannot \"save\" the block as a proc to be run later, unless the block is\nconfigured to run in the \"wrapped object's\" context. This is simply because we\nhave access to the caller only while the caller is making a method call. After\nthe call is done, we no longer have access to that context, and there's no\nguarantee that the caller or its Ractor even exists anymore. In particular,\nthis means that the common Ruby idiom of using blocks to define callbacks (that\nrun in the context of the code defining the callback) can generally not be done\nthrough a wrapper.\n\nIn Ruby, it is legal (although not considered very good practice) to do a\nnon-local `return` from inside a block. Assuming the block isn't being defined\nvia a lambda, this causes a return from the method *surrounding* the call that\nincludes the block. Ractor::Wrapper cannot reproduce this behavior. Attempting\nto `return` within a block that was passed to Ractor::Wrapper will result in an\nexception.\n\n### Re-entrancy via blocks\n\nOne final known issue with Ractor::Wrapper is that it does not currently handle\nre-entrancy resulting from a block making another call to the object. That is,\nif a method on a wrapper is called, and it yields to a block that runs back in\nthe caller's context, and that block then makes another method call to the same\nwrapper, now there's a new method call request when the first method is still\nbeing handled (and blocked because it's yielding to the block). Unless the\nwrapper is configured with enough threads that another thread can pick up the\nnew method call, this will deadlock the wrapper: the original method call is\nblocked because the yield is not complete, but the yield will never complete\nbecause the new method cannot run until the original method has completed.\n\nI believe this issue is solvable by retooling the internal method scheduling to\nuse fibers, and I have filed a to-do item to address it in the future\n(https://github.com/dazuma/ractor-wrapper/issues/12). Until then, I do not\nrecommend making additional calls to a wrapper from within a yielded block.\n\n## Contributing\n\nDevelopment is done in GitHub at https://github.com/dazuma/ractor-wrapper.\n\n*   To file issues: https://github.com/dazuma/ractor-wrapper/issues.\n*   For questions and discussion, please do not file an issue. Instead, use the\n    discussions feature: https://github.com/dazuma/ractor-wrapper/discussions.\n*   Pull requests are welcome, but the library is highly experimental at this\n    stage, and I recommend discussing features or design changes first before\n    implementing.\n\nThe library uses [toys](https://dazuma.github.io/toys) for testing and CI. To\nrun the test suite, `gem install toys` and then run `toys ci`. You can also run\nunit tests, rubocop, and build tests independently.\n\n## License\n\nCopyright 2021-2026 Daniel Azuma\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\nIN THE SOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdazuma%2Fractor-wrapper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdazuma%2Fractor-wrapper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdazuma%2Fractor-wrapper/lists"}