File: transaction.rb

package info (click to toggle)
rails 2%3A7.2.2.1%2Bdfsg-7
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 43,352 kB
  • sloc: ruby: 349,799; javascript: 30,703; yacc: 46; sql: 43; sh: 29; makefile: 27
file content (132 lines) | stat: -rw-r--r-- 4,836 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# frozen_string_literal: true

require "active_support/core_ext/digest"

module ActiveRecord
  # Class specifies the interface to interact with the current transaction state.
  #
  # It can either map to an actual transaction/savepoint, or represent the
  # absence of a transaction.
  #
  # == State
  #
  # We say that a transaction is _finalized_ when it wraps a real transaction
  # that has been either committed or rolled back.
  #
  # A transaction is _open_ if it wraps a real transaction that is not finalized.
  #
  # On the other hand, a transaction is _closed_ when it is not open. That is,
  # when it represents absence of transaction, or it wraps a real but finalized
  # one.
  #
  # You can check whether a transaction is open or closed with the +open?+ and
  # +closed?+ predicates:
  #
  #  if Article.current_transaction.open?
  #    # We are inside a real and not finalized transaction.
  #  end
  #
  # Closed transactions are `blank?` too.
  #
  # == Callbacks
  #
  # After updating the database state, you may sometimes need to perform some extra work, or reflect these
  # changes in a remote system like clearing or updating a cache:
  #
  #   def publish_article(article)
  #     article.update!(published: true)
  #     NotificationService.article_published(article)
  #   end
  #
  # The above code works but has one important flaw, which is that it no longer works properly if called inside
  # a transaction, as it will interact with the remote system before the changes are persisted:
  #
  #   Article.transaction do
  #     article = create_article(article)
  #     publish_article(article)
  #   end
  #
  # The callbacks offered by ActiveRecord::Transaction allow to rewriting this method in a way that is compatible
  # with transactions:
  #
  #   def publish_article(article)
  #     article.update!(published: true)
  #     Article.current_transaction.after_commit do
  #       NotificationService.article_published(article)
  #     end
  #   end
  #
  # In the above example, if +publish_article+ is called inside a transaction, the callback will be invoked
  # after the transaction is successfully committed, and if called outside a transaction, the callback will be invoked
  # immediately.
  #
  # == Caveats
  #
  # When using after_commit callbacks, it is important to note that if the callback raises an error, the transaction
  # won't be rolled back as it was already committed. Relying solely on these to synchronize state between multiple
  # systems may lead to consistency issues.
  class Transaction
    def initialize(internal_transaction) # :nodoc:
      @internal_transaction = internal_transaction
      @uuid = nil
    end

    # Registers a block to be called after the transaction is fully committed.
    #
    # If there is no currently open transactions, the block is called
    # immediately, unless the transaction is finalized, in which case attempting
    # to register the callback raises ActiveRecord::ActiveRecordError.
    #
    # If the transaction has a parent transaction, the callback is transferred to
    # the parent when the current transaction commits, or dropped when the current transaction
    # is rolled back. This operation is repeated until the outermost transaction is reached.
    #
    # If the callback raises an error, the transaction remains committed.
    def after_commit(&block)
      if @internal_transaction.nil?
        yield
      else
        @internal_transaction.after_commit(&block)
      end
    end

    # Registers a block to be called after the transaction is rolled back.
    #
    # If there is no currently open transactions, the block is not called. But
    # if the transaction is finalized, attempting to register the callback
    # raises ActiveRecord::ActiveRecordError.
    #
    # If the transaction is successfully committed but has a parent
    # transaction, the callback is automatically added to the parent transaction.
    #
    # If the entire chain of nested transactions are all successfully committed,
    # the block is never called.
    #
    # If the transaction is already finalized, attempting to register a callback
    # will raise ActiveRecord::ActiveRecordError.
    def after_rollback(&block)
      @internal_transaction&.after_rollback(&block)
    end

    # Returns true if the transaction exists and isn't finalized yet.
    def open?
      !closed?
    end

    # Returns true if the transaction doesn't exist or is finalized.
    def closed?
      @internal_transaction.nil? || @internal_transaction.state.finalized?
    end

    alias_method :blank?, :closed?

    # Returns a UUID for this transaction or +nil+ if no transaction is open.
    def uuid
      if @internal_transaction
        @uuid ||= Digest::UUID.uuid_v4
      end
    end

    NULL_TRANSACTION = new(nil).freeze
  end
end