Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for setting meta without transaction block #143

Merged
merged 10 commits into from
Nov 29, 2019
94 changes: 68 additions & 26 deletions lib/logidze/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@
module Logidze # :nodoc:
# Provide methods to attach meta information
module Meta
def with_meta(meta, &block)
MetaTransaction.wrap_with(meta, &block)
def with_meta(meta, transactional: true, &block)
palkan marked this conversation as resolved.
Show resolved Hide resolved
wrapper = transactional ? MetaWithTransaction : MetaWithoutTransaction
wrapper.wrap_with(meta, &block)
end

def with_responsible(responsible_id, &block)
def with_responsible(responsible_id, transactional: true, &block)
return yield if responsible_id.nil?

meta = {Logidze::History::Version::META_RESPONSIBLE => responsible_id}
with_meta(meta, &block)
with_meta(meta, transactional: transactional, &block)
end

class MetaTransaction # :nodoc:
class MetaWrapper # :nodoc:
def self.wrap_with(meta, &block)
new(meta, &block).perform
end
Expand All @@ -29,12 +30,65 @@ def initialize(meta, &block)
end

def perform
return if block.nil?
raise ArgumentError, "Block must be given" unless block
return block.call if meta.nil?

ActiveRecord::Base.transaction { call_block_in_meta_context }
call_block_in_meta_context
end

def current_meta
meta_stack.reduce(:merge) || {}
end

def meta_stack
Thread.current[:meta] ||= []
Thread.current[:meta]
end

def encode_meta(value)
connection.quote(ActiveSupport::JSON.encode(value))
end

def pg_reset_meta_param(prev_meta)
if prev_meta.empty?
pg_clear_meta_param
else
pg_set_meta_param(prev_meta)
end
end
end

class MetaWithTransaction < MetaWrapper # :nodoc:
private

def call_block_in_meta_context
ActiveRecord::Base.transaction do
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do the following:

  • Implement call_block_in_meta_context in the base class:
def call_block_in_meta_context
  prev_meta = current_meta

  meta_stack.push(meta)

  pg_set_meta_param(current_meta)
  result = block.call
  result
ensure
  pg_reset_meta_param(prev_meta)
  meta_stack.pop
end
  • Here we can override this method by wrapping super with transaction:
def call_block_in_meta_context
  ActiveRecord::Base.transaction { super }
end
  • Drop call_block_in_meta_context in MetaWithoutTransaction (the current implementation is not complete — we should take meta_stack into account; the base implementation above would work in non-transactional case without any change)

begin
prev_meta = current_meta

meta_stack.push(meta)

pg_set_meta_param(current_meta)
result = block.call
pg_reset_meta_param(prev_meta)

result
ensure
meta_stack.pop
end
end
end

def pg_set_meta_param(value)
connection.execute("SET LOCAL logidze.meta = #{encode_meta(value)};")
end

def pg_clear_meta_param
connection.execute("SET LOCAL logidze.meta TO DEFAULT;")
end
end

class MetaWithoutTransaction < MetaWrapper # :nodoc:
private

def call_block_in_meta_context
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, we can move this to the base class (MetaWrapper).

Expand All @@ -44,36 +98,24 @@ def call_block_in_meta_context

pg_set_meta_param(current_meta)
result = block.call
pg_reset_meta_param(prev_meta)

result
ensure
pg_reset_meta_param(prev_meta)
meta_stack.pop
end

def current_meta
meta_stack.reduce(:merge) || {}
end

def meta_stack
Thread.current[:meta] ||= []
Thread.current[:meta]
end

def pg_set_meta_param(value)
encoded_meta = connection.quote(ActiveSupport::JSON.encode(value))
connection.execute("SET LOCAL logidze.meta = #{encoded_meta};")
connection.execute("SET logidze.meta = #{encode_meta(value)};")
end

def pg_reset_meta_param(prev_meta)
if prev_meta.empty?
connection.execute("SET LOCAL logidze.meta TO DEFAULT;")
else
pg_set_meta_param(prev_meta)
end
def pg_clear_meta_param
connection.execute("SET logidze.meta TO DEFAULT;")
end
end

private_constant :MetaTransaction
private_constant :MetaWrapper
private_constant :MetaWithTransaction
private_constant :MetaWithoutTransaction
end
end
22 changes: 22 additions & 0 deletions spec/integrations/meta_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,28 @@
expect(subject.at_version(3).meta).to eq meta2
end
end

context "when transactional:false" do
it "resets meta setting after block finishes" do
# subject is a newly created user
Logidze.with_meta(meta, transactional: false) do
expect(subject.reload.meta).to eq meta
end

# create another one and check that meta is nil here
expect(User.create!(name: "test", age: 10, active: false).reload.meta).to be_nil
end

it "recovers after exception" do
ignore_exceptions do
Logidze.with_meta(meta, transactional: false) do
CustomUser.create!
end
end

expect(subject.reload.meta).to be_nil
end
end
end

describe ".with_responsible" do
Expand Down