{"id":24264602,"url":"https://github.com/stex/petra","last_synced_at":"2025-09-24T01:30:57.824Z","repository":{"id":54687393,"uuid":"46733422","full_name":"stex/petra","owner":"stex","description":"[POC] Continuation based temporarily persisted transactions in Ruby","archived":false,"fork":false,"pushed_at":"2023-01-19T09:25:00.000Z","size":561,"stargazers_count":6,"open_issues_count":7,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-08-09T20:55:17.296Z","etag":null,"topics":["proof-of-concept","ruby","transaction"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-11-23T16:33:34.000Z","updated_at":"2021-02-03T23:08:20.000Z","dependencies_parsed_at":"2023-02-10T22:01:23.271Z","dependency_job_id":null,"html_url":"https://github.com/stex/petra","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/stex/petra","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stex%2Fpetra","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stex%2Fpetra/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stex%2Fpetra/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stex%2Fpetra/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stex","download_url":"https://codeload.github.com/stex/petra/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stex%2Fpetra/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":276678844,"owners_count":25684803,"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","status":"online","status_checked_at":"2025-09-23T02:00:09.130Z","response_time":73,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["proof-of-concept","ruby","transaction"],"created_at":"2025-01-15T09:32:28.490Z","updated_at":"2025-09-24T01:30:57.424Z","avatar_url":"https://github.com/stex.png","language":"Ruby","readme":"[![Build Status](https://travis-ci.org/Stex/petra.svg?branch=master)](https://travis-ci.org/Stex/petra)\n[![Gem Version](https://badge.fury.io/rb/petra_core.svg)](https://badge.fury.io/rb/petra_core)\n\n# petra\n\u003cimg src=\"petra.png\" width=\"200\" align=\"right\" /\u003e\n\nPetra is a proof-of-concept for **pe**rsisted **tra**nsactions in Ruby with (hopefully) full ACI(D) properties.\n\nPlease note that this was created during my master's thesis in 2016 and hasn't been extended a lot since then except for a few coding style fixes. I would write a lot of stuff differently today, but the main concept is still interesting enough.\n\nIt allows starting a transaction without committing it and resuming it at a later time, even in another process - given the used objects provide identifiers other than `object_id`.\n\nIt should work with every Ruby object and can be extended to work with web frameworks like Ruby-on-Rails as well (a POC of RoR integration can be found at [stex/petra-rails](https://github.com/stex/petra-rails)). \n\nThis README only covers parts of what `petra` has to offer. Feel free to dive into the code, everything should be commented accordingly.\n\nLet's take a look at how `petra` is used:\n\n```ruby\nclass SimpleUser\n  attr_accessor :first_name, :last_name\n  \n  def name\n    \"#{first_name} #{last_name}\"\n  end\n  \n  # ... configuration, see below\nend\n\nuser = SimpleUser.petra.new('John', 'Doe')\n\n# Start a new transaction and start changing attributes\nPetra.transaction(identifier: 'tr1') do\n  user.first_name = 'Foo'\nend\n\n# No changes outside the transaction yet...\nputs user.name #=\u003e 'John Doe'\n\n# Continue the same transaction\nPetra.transaction(identifier: 'tr1') do\n  puts user.name #=\u003e 'Foo Doe'\n  user.last_name = 'Bar'\nend\n\n# Another transaction changes a value already changed in 'tr1'\nPetra.transaction do\n  user.first_name = 'Moo'\n  Petra.commit!\nend\n\nputs user.name #=\u003e 'Moo Doe'\n\n# Try to commit our first transaction\nPetra.transaction(identifier: 'tr1') do\n  puts user.name\n  Petra.commit!\nrescue Petra::WriteClashError =\u003e e\n  # =\u003e \"The attribute `first_name` has been changed externally and in the transaction. (Petra::WriteClashError)\"\n  # Let's use our value and go on with committing the transaction\n  e.use_ours!\n  e.continue!\nend\n\n# The actual object is updated with the values from tr1\nputs user.name #=\u003e 'Foo Bar'\n```\n\nWe just used a simple Ruby object inside a transaction which was even split into multiple sections! \n\n(The full example can be found at [`examples/showcase.rb`](https://github.com/Stex/petra/blob/master/examples/showcase.rb))\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n## TOC\n\n- [Installation](#installation)\n- [Basic Usage](#basic-usage)\n  - [Starting/Resuming a transaction](#startingresuming-a-transaction)\n  - [Transactional Objects and their Configuration](#transactional-objects-and-their-configuration)\n  - [Commit / Rollback / Reset / Retry](#commit--rollback--reset--retry)\n- [Reacting to external changes](#reacting-to-external-changes)\n  - [An attribute we previously read was changed externally](#an-attribute-we-previously-read-was-changed-externally)\n  - [An attribute we changed in our transaction was also changed externally](#an-attribute-we-changed-in-our-transaction-was-also-changed-externally)\n  - [`continue!`?](#continue)\n- [Full Configuration Options](#full-configuration-options)\n  - [Global Options](#global-options)\n  - [Class Specific Options](#class-specific-options)\n- [Extending `petra`](#extending-petra)\n  - [Class Proxies](#class-proxies)\n  - [Module Proxies](#module-proxies)\n  - [Persistence Adapters](#persistence-adapters)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Installation\n\nSimply add the following line to your gemfile:\n\n```ruby\ngem 'petra_core', require: 'petra'\n```\n    \nUnfortunately, the gem name `petra` is already taken and `petra-core` would express that this gem is extending it, so \nI went for an underscore for now. It's hard finding nice-sounding gem names which are not yet taken nowadays :/\n\n## Basic Usage\n\n### Starting/Resuming a transaction\n\nWhenver you call `Petra.transaction`, a *transaction section* is started. If you pass in an identifier and a matching transaction already exists, it will be resumed instead.\n\n```ruby\n# Starting a new transaction with an auto-generated identifier\ntr_id = Petra.transaction {}\n\n# Resuming the transaction\nPetra.transaction(identifier: tr_id) {}\n```\n\n### Transactional Objects and their Configuration\n\nAlthough `petra` is seemingly able to use every Ruby object inside a transaction, it does not patch these objects in any way by e.g. overriding their getters and setters. Instead, a transparent proxy is used:\n\n```\n# Normal instance of SimpleUser\nuser = SimpleUser.new\n\n# ObjectProxy, can now be used inside and outside of transactions\nuser = SimpleUser.petra.new # or: user = SimpleUser.new.petra\n```\n\nIn its current version, `petra` has to be told about the meaning of the different methods of a class to be used inside a transaction.  \nThis decision was made as there are no strict conventions regarding method names in Ruby (e.g. `getX`/`setX` in Java).\n\n`petra` knows about 5 different kinds of methods:\n\n1. **Attribute Readers** which retrieve a current attribute value\n2. **Attribute Writers** which set a new attribute value\n3. **Dynamic Attribute Readers** which a composite methods like `name` (not an actual attribute, but use attributes interally)\n4. **Persistence Methods** which save changes made to the object (think of `ActiveRecord::Base#save`)\n5. **Destruction Methods** which remove the object\n\n\nLet's create a configuration for `SimpleUser`:\n\n```ruby\nPetra.configure do\n  configure_class SimpleUser do\n    # Tell petra about our available attribute readers\n    attribute_reader? do |method_name|\n      %w[first_name last_name].include?(method_name.to_s)\n    end\n    \n    # Do the same for attribute writers\n    attribute_writer? do |method_name|\n      %w[first_name= last_name=].include?(method_name.to_s)\n      # also possible here: `method_name.last == '='`\n    end    \n    \n    # Define which methods are used to persist instances of SimpleUser\n    persistence_method? do |method_name|\n      %w[first_name= last_name=].include?(method_name.to_s)\n    end    \n\n    # `name` uses attributes internally\n    dynamic_attribute_reader? do |method_name|\n      %[name].include?(method_name.to_s)\n    end\n  end\nend\n```\n\nAs you may have noticed, we used our `attribute_writer`s twice in this configuration: Once as actual attribute writers and once as persistence method. This was done to keep the example above as small as possible.\n\nThe same could have been achieved by setting up a no-op method and configuring it accordingly:\n\n```ruby\n# SimpleUser\ndef save; end\n\n# Configuration\npersistence_method { |method_name| %w[save].include?(method_name.to_s) } \n\n# Usage\nPetra.transaction do\n  user.first_name = 'Foo'\n  user.save\nend\n```\n\nIn this case, not calling `save` inside the transaction would have lead to the loss of everything we did inside the transaction section.\n\n### Commit / Rollback / Reset / Retry\n\n#### Commit\n\nTransactions can be committed by calling `Petra.commit!` inside a `Petra.transaction` block.  \nIt will leave the transaction block afterwards and not execute anything left in it:\n\n```ruby\nPetra.transaction do\n  Petra.commit!\n  puts 'I will never be shown!'\nend\n```\n\n#### Rollback\n\nA rollback can be triggered by either raising `Petra::Rollback` or simply any other uncaught `StandardError`. The difference is that `Petra::Rollback` will be swallowed by the transaction processing (like `ActiveRecord::Rollback` does), while any other error will be re-raised.\n\nTriggering a rollback will undo all changes made **in the current section** of the transaction. All previous sections are not affected.\n\n```ruby\nPetra.transaction(identifier: 'tr1') do\n  user.first_name = 'Foo'\nend\n\nPetra.transaction(identifier: 'tr1') do\n  user.last_name = 'Bar'\n  fail Petra::Rollback\nend\n```\n\nIn this example, only the change to `user#last_name` is lost.\n\n#### Reset\n\nA reset can be triggered by raising `Petra::Reset`. It works like a rollback, but will clear **the whole transaction**.\n\n```ruby\nPetra.transaction(identifier: 'tr1') do\n  user.first_name = 'Foo'\nend\n\nPetra.transaction(identifier: 'tr1') do\n  user.last_name = 'Bar'\n  fail Petra::Reset\nend\n```\n\nHere, all changes to `user` are lost.\n\n#### Retry\n\nA retry means that the current transaction block should be retried again after a rollback.  \n\n```ruby\nPetra.transaction(identifier: 'tr1') do\n  user.last_name = 'Bar'\n  fail Petra::Retry if some_condition\nend\n```\n\n## Reacting to external changes\n\nAs the transaction is working in isolation on its own data set, it might happen that the original objects outside the transaction are changed in the meantime, e.g. by another transaction's commit:\n\n```ruby\nPetra.transaction(identifier: 'tr1') do\n  user.first_name = 'Foo'\nend\n\nPetra.transaction(identifier: 'tr2') do\n  user.first_name = 'Moo'\n  Petra.commit!\nend\n\nPetra.transaction(identifier: 'tr1') do\n  # we don't know about the external change here and would\n  # possibly override it\nend\n```\n\n`petra` reacts to these external changes and raises a corresponding exception. This exception allows the developer to solve the conflicts based on his current context.\n\nThe exception is thrown either when the attribute is used again or during the commit phase. Not handling any of these exception yourself will result in a transaction reset.\n\nEach error described below shares a few common methods to control the further transaction flow:\n\n```ruby\nPetra.transaction(identifier: 'tr1') do\n  begin\n    ...\n  rescue Petra::ValueComparisionError =\u003e e # Superclass of ReadIntegrityError and WriteClashError\n    e.object          #=\u003e the object which was changed externally\n    e.attribute       #=\u003e the name of the changed attribute\n    e.external_value  #=\u003e the new external value\n  \n    e.retry!    # Runs the current transaction block again\n    e.rollback! # Dismisses all changes in the current section, continues after transaction block\n    e.reset!    # Resets the whole transaction, continues after transaction block\n    e.continue! # Continues with executing the current transaction block\n  end\nend\n```\n\nPlease note that in most cases calling `rollback!`, `retry!` or `continue!` without any other exception specific method will result in the same error again the next time.\n\n### An attribute we previously read was changed externally\n\nA `ReadIntegrityError` is thrown if one transaction reads an attribute value which is then changed externally:\n\n```ruby\nPetra.transaction(identifier: 'tr1') do\n  user.last_name = 'the first' if user.first_name = 'Karl'\nend\n\nuser.first_name = 'Olaf'\n\nPetra.transaction(identifier: 'tr1') do\n  user.first_name \n  #=\u003e Petra::ReadIntegrityError: The attribute `first_name` has been changed externally.\nend\n```\n\nWhen triggering a `ReadIntegrityError`, you can choose to acknowledge/ignore the external change. Doing so will suppress further errors as long as the external value does not change again.\n\n```ruby\nbegin\n...\nrescue Petra::ReadIntegrityError =\u003e e\n  e.last_read_value #=\u003e the value we got when last reading the attribute\n\n  e.ignore!(update_value: true)  # we acknowledge the external change and use the new value in our transaction from now on\n  e.ignore!(update_value: false) # we keep our old value and simply ignore the external change.\n  e.retry!\nend\n```\n\n### An attribute we changed in our transaction was also changed externally\n\nA `WriteClashError` is thrown whenever an attribute we changed inside one of our transaction sections was also changed externally:\n\n```ruby\nPetra.transaction(identifier: 'tr1') do\n  user.first_name = 'Foo'\nend\n\nuser.first_name = 'Moo'\n\nPetra.transaction(identifier: 'tr1') do\n  user.first_name\n  #=\u003e Petra:WriteClashError: The attribute `first_name` has been changed externally and in the transaction.\nend\n```\n\nAs both sides changed the attribute value, we have to decided which one to use further in most cases (or completely reset the transaction):\n\n```ruby\nbegin\n...\nrescue Petra::WriteClashError =\u003e e\n  e.our_value   #=\u003e the value we set the attribute to\n  e.their_value #=\u003e the new external value\n\n  e.use_theirs! # undo every change we made to the attribute in this transaction\n  e.use_ours!   # Ignore the external change, use our value\n  e.retry!\nend\n```\n\n### `continue!`?\n\nAs mentioned above, `petra` allows the developer to jump back into the transaction after an error was resolved.  \nThis is done by using Ruby's [Continuation](https://ruby-doc.org/core-2.5.0/Continuation.html) which basically saves a copy of the stack at the time the exception happened. This copy can then be restored if the developer decides to continue the execution.\n\nI'd personally keep everything regarding continuations far away from production code, but they are a very interesting concept (which will most likely be removed with Ruby 3.0 :/ ). `examples/continuation_error.rb` shows one of the drawbacks which could lead to a long time of debugging.\n\n```ruby\nbegin\n  simple_user.first_name = 'Foo'\n  simple_user.save\nrescue Petra::WriteClashError =\u003e e\n  e.use_ours!\n  # Jumps back to `simple_user.save` without a retry\n  e.continue!\nend\n```\n\n## Full Configuration Options\n\n### Global Options\n\n#### `persistence_adapter`\n\n```ruby\nPetra.configure do\n  persistence_adapter :file\n  persistence_adapter.storage_directory = '/tmp/petra'\nend\n```\n\nSpecifies the persistence adapter and its possible options.  \nPetra only includes a file system based adapter by default.\n\n#### `instantly_fail_on_read_integrity_errors`\n\n```ruby\nPetra.configure do\n  instantly_fail_on_read_integrity_errors false\nend\t\n```\n\n`petra` can be set to optimistic transaction handling. This means, that a transaction is only checked\nfor possible external changes during the commit phase.\n\nBy default, a corresponding error is thrown directly when the attribute is accessed again within the transaction.\n\n#### `log_level`\n\n```ruby\nPetra.configure do\n  log_level :debug | :info | :warn | :error\nend\n```\n\nSpecifies the log level `petry` should use. \n\n* `:debug`\n\t* Information about all methods called on an object proxy and their results\n\t* Attribute reads and changes\n\t* Acquired and released locks\n\t* The creation of transaction log entries\n* `:info`\n\t* Starting and persisting a transaction\n\t* Committing a transaction\n\t* Triggering a rollback on a transaction\n* `:warn`\n\t* Forced transaction resets\n\n### Class Specific Options\n\nApart from the already mentioned ones, the following class specific options are available:\n\n#### `proxy_instances`\n\nDetermines whether `petra` should automatically create proxies for instances of the configured class when they are accessed from within an existing object proxy.\n\n```ruby\nPetra.configure do\n  configure_class SimpleUser do\n    proxy_instances true\n  end\n  \n  # Do not create a proxy for strings. Otherwise, calling `SimpleUser#first_name` would result in a string object proxy\n  configure_class String do\n    proxy_instances false\n  end\nend\n```\n\n#### `use_specialized_proxy`\n\n`petra` contains a very basic `ObjectProxy` implementation which works fine with most ruby objects, but has to be configured.  \nFor more advanced classes, it is advised to create a specialized proxy (see `petra-rails`).\n\nBy default, `petra` will use the specialized version if available, but can be forced to use the basic object proxy instead:\n\n```ruby\nPetra.configure do\n  configure_class ActiveRecord::Base do\n    use_specialized_proxy false\n  end\nend\n```\n\n#### `mixin_module_proxies`\n\n`petra` does not only support proxies for certain classes, but also for mixins. This allows a developer to define a proxy which is automatically used for every class which contains a certain module.\n\nBy default, `petra` contains an `Enumerable` proxy which automatically wraps its entries in object proxies.\n\nThe automatic inclusion of these module proxies can be disabled:\n\n```ruby\nPetra.configure do\n  configure_class Array do\n    mixin_module_proxies false\n  end\nend\n```\n\n#### `id_method`\n\nSpecifies the method to retrieve an identifier for instances of the configured class.\n\nBy default, `object_id` is used, which of course is very limited. \n\n```ruby\nPetra.configure do\n  configure_class ActiveRecord::Base do\n    id_method :id\n    # or\n    id_method do |obj|\n      obj.id\n    end\n  end\nend\n```\n\n#### `lookup_method`\n\nBasically the counterpart of `id_method`. Specifies the class method which can be used to retrieve an instance of the configured class when providing the corresponding identifier.\n\nIt defaults to `ObjectSpace._id2ref` which returns an object by its `object_id`.\n\n```ruby\nPetra.configure do\n  configure_class ActiveRecord::Base do\n    lookup_method :find\n  end\nend\n```\n\n#### `init_method`\n\nSpecifies the method to initialize a new instance of the configured class (or one of its descendants).  \nIt is used to automatically re-initialize objects used (and persisted) in a previous section and works the same way as lookup_method.\n\n```ruby\nPetra.configure do\n  configure_class Array do\n    init_method :new\n  end\nend\n```\n\n## Extending `petra`\n\n`petra` can be easily extended to a certain extent as seen in [stex/petra-rails](https://github.com/stex/petra-rails).\n\n### Class Proxies\n\nAs mentioned above, some classes are too complicated to be configured using the basic `ObjectProxy`.\n\nLet's define a basic example for such a class:\n\n```ruby\nclass SimpleRecord\n  def self.create(attributes = {})\n    new(attributes).save\n  end\n  \n  def save\n    # some persistence logic\n  end\nend\n```\n\nIn this example, `#create` is a method we cannot configure easily as it doesn't match any of the available method types in `ObjectProxy`. Instead. it is a combination of attribute writers and persistence methods.\n\nTo be taken into account as a custom object proxy, a class has to comply to the following rules:\n\n1. It has to be defined inside `Petra::Proxies`\n2. It has to inherit from `Petra::Proxies::ObjectProxy`\n3. It has to define the class names it may be applied to in a constant named `CLASS_NAMES`\n\nLet's define the corresponding proxy for `SimpleRecord`:\n\n```ruby\nmodule Petra\n  module Proxies\n    class SimpleRecordProxy \u003c ObjectProxy\n      CLASS_NAMES = %w[SimpleRecord].freeze\n\n      def create(attributes = {})\n        # This method may only be called on class, not on instance level\n        class_method!\n\n        # Use ObjectProxy's basic `new` method without any arguments\n        new.tap do |obj|\n          # Tell our transaction that we initialized a new object.\n          # This wasn't done in the previous examples as we were working on the\n          # `ObjectSpace` with objects defined outside the transaction.\n          transaction.log_object_initialization(o, method: 'new')\n\n          # Apply the attribute writes inside the transaction\n          attributes.each do |k, v|\n            __set_attribute(k, v)\n          end\n\n          # #create automatically persists a record, we therefore have to\n          # tell our transaction to log this action.\n          transaction.log_object_persistence(o, method: 'save')\n        end\n      end\n\n      def save\n        transaction.log_object_persistence(self, method: 'save')\n      end\n    end\n  end\nend\n```\n\nSee [petra-rails's ActiveRecordProxy](https://github.com/Stex/petra-rails/blob/master/lib/petra/proxies/active_record_proxy.rb) for a full example.\n\n### Module Proxies\n\nAs mentioned above, module proxies can be used to define proxy functionality for all classes which include a certain module.  \nInternally, these modules are included into the singleton class of our object proxies, meaning that one instance of a proxy could include a certain module, the other doesn't.\n\nA module proxy has to comply to the following rules:\n\n1. It has to be defined in `Petra::Proxies`\n2. It has to include `Petra::Proxies::ModuleProxy`\n3. It has to define a constant named `MODULE_NAMES` which contains the modules it is applicable for.\n\nLet's take a look at `petra`'s `EnumerableProxy`:\n\n```ruby\nmodule Petra\n  module Proxies\n    module EnumerableProxy\n      include ModuleProxy\n      MODULE_NAMES = %w[Enumerable].freeze\n\n      # Specifying an `INCLUDES` constant leads to instances of the resulting proxy\n      # automatically including the given modules - in this case, every proxy which handles\n      # an Enumerable will automatically be an Enumerable as well\n      INCLUDES = [Enumerable].freeze\n\n      # ModuleProxies may specify an `InstanceMethods` and a `ClassMethods` sub-module.\n      # Their methods will be included/extended accordingly.\n      module InstanceMethods\n        #\n        # We have to define our own #each method for the singleton class' Enumerable\n        # It basically just wraps the original enum's entries in proxies and executes\n        # the \"normal\" #each\n        #\n        def each(\u0026block)\n          Petra::Proxies::EnumerableProxy.proxy_entries(proxied_object).each(\u0026block)\n        end\n      end\n\n      #\n      # Ensures the the objects yielded to blocks are actually petra proxies.\n      # This is necessary as the internal call to +each+ would be forwarded to the\n      # actual Enumerable object and result in unproxied objects.\n      #\n      # This method will only proxy objects which allow this through the class config\n      # as the enum's entries are seen as inherited objects.\n      # `[]` is used as method causing the proxy creation as it's closest to what's actually happening.\n      #\n      # @return [Array\u003cPetra::Proxies::ObjectProxy\u003e]\n      #\n      def self.proxy_entries(enum, surrogate_method: '[]')\n        enum.entries.map { |o| o.petra(inherited: true, configuration_args: [surrogate_method]) }\n      end\n    end\n  end\nend\n```\n\nPlease take a look at [`lib/petra/proxies/abstract_proxy.rb`](https://github.com/Stex/petra/blob/master/lib/petra/proxies/abstract_proxy.rb) for more information regarding how proxies are chosen and built.\n\n### Persistence Adapters\n\nFor its transaction handling, `petra` needs access to a storage with atomic write operations to store its transaction logs as well as being able to lock certain resources (during commit phase, no other transaction may have access to certain resources).\n\n[`Petra::PersistenceAdapters::Adapter`](https://github.com/Stex/petra/blob/master/lib/petra/persistence_adapters/adapter.rb) provides an interface for classes which provide this functionality. [`FileAdapter`](https://github.com/Stex/petra/blob/master/lib/petra/persistence_adapters/file_adapter.rb) is the reference implementation which uses the file system and UNIX file locks.\n\n#### Required Methods\n\n**`persist!`**\n\nSaves all available transaction log entries to the storage.\nLog entries are added using `#enqueue(entry)` and available as `queue` inside your adapter instance.\n\n* A transaction lock has to be applied\n* Entries have to be marked as persisted afterwards using `entry.mark_as_persisted!`\n\n\n**`transaction_identifiers`**\n\nShould return the identifiers of all transactions which were started, but not yet committed.\n\n**`savepoints(transaction)`**\n\nShould return all savepoints (section identifiers) for the given transaction,\n\n**`log_entries(section)`**\n\nShould return all log entries which were persisted for the given section in the past.\n\n**`reset_transaction(transaction)`**\n\nRemoves all information currently stored regarding the given transaction\n\n**`with_global_lock(suspend:, \u0026block)`**\n\nAcquires a global lock (only one thread may hold it at the same time), runs the given block and releases the global lock again.\n\nIf `suspend` is set to `true`, the execution will wait for the lock to be available, otherwise, a `Petra::LockError` is thrown if the lock is not available.\n\nYou have to make sure that the lock is freed again if an error occurs within the given block or your own implementation.\n\n**`with_transaction_lock(transaction, suspend:)`**\n\nAcquires a lock on the given transaction.\n\n**`with_object_lock(object, suspend:)`**\n\nAcquires a lock on the given Object (Proxy). \n\nMake sure that your implementation allows one thread locking the resource multiple times without stalling.\n\n```ruby\nwith_object_lock(obj1) do\n  with_object_lock(obj1) do # Should work as we already hold the lock\n   ...\n  end\nend \n```\n\n\n#### Registering a new adapter\n\nSimilar to Rails' mailer adapters, new adapter can be registered under a given name and be used in `petra`'s configuration afterwards:\n\n```ruby\nPetra::PersistenceAdapters::Adapter.register_adapter(:redis, RedisAdapter)\n\nPetra.configure do\n  persistence_adapter :redis\nend\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstex%2Fpetra","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstex%2Fpetra","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstex%2Fpetra/lists"}