Skip to content

Instantly share code, notes, and snippets.

@simi
Last active May 17, 2022 06:53
Show Gist options
  • Save simi/2789ef5138608b0f18c181d921a08338 to your computer and use it in GitHub Desktop.
Save simi/2789ef5138608b0f18c181d921a08338 to your computer and use it in GitHub Desktop.
# INFO: works well with Ruby 3.0.1
# hangs on any Ruby including https://github.com/ruby/ruby/commit/f9196de1dee6f5ab8b6fe115070b92775a3500fe
# since the yielding code in each method is running in different Fiber (due to be wrapped into Enumerator and next is used)
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Activate the gem you are reporting the issue against.
gem "activerecord", "~> 7.0.0"
gem "sqlite3"
end
require "active_record"
require "minitest/autorun"
require "logger"
require 'csv'
# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :users, force: true do |t|
t.string :name
end
end
class User < ActiveRecord::Base
end
class Exporter
attr_reader :scope
def initialize(scope)
@scope = scope
end
def each
return enum_for(__method__) unless block_given?
yield CSV.generate_line(["name"])
scope.in_batches(of: 10).each_record do |user|
yield CSV.generate_line([user.name])
end
end
end
class LockTest < Minitest::Test
def test_my_problem
User.insert_all(100.times.map { |i| {name: "User ##{i}"} })
User.transaction do
# simulate something affecting data to be visible only in this transaction
sleep 2
# and "streamed" export
enum = Exporter.new(User.all).each
# dummy example of consuming iterator
# in real code it is wrapped into IO-like API and consumed as IO using buffered read
# output is being zipped and uploaded in chunks to GCS
#
# for IO wrapper approach similar to https://github.com/lautis/piperator/blob/master/lib/piperator/io.rb is used
#
# iteration happens only inside of the transaction, enum is not leaked outside
File.open('/tmp/export.csv', 'w+') do |file|
loop do
data = enum.next
sleep 0.05 # simulate more work in the "streaming" chain
file.write(data)
rescue StopIteration
break
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment