Skip to content

Instantly share code, notes, and snippets.

@sergueif
Last active June 5, 2019 22:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sergueif/9b428de5cab059a27ca0392dc9a0e091 to your computer and use it in GitHub Desktop.
Save sergueif/9b428de5cab059a27ca0392dc9a0e091 to your computer and use it in GitHub Desktop.
Value persistence across active records with lazy-loading
class CreateValueRecords < ActiveRecord::Migration[5.1]
def change
create_table :value_records, id: false do |t|
t.jsonb :body, null: false, default: {}
t.string :key
t.timestamps
end
execute "ALTER TABLE value_records ADD PRIMARY KEY (key);"
end
end
class TestCompressedTeam
def initialize(name, players=[])
@name = name
@players = players
end
attr_reader :name, :players
def ==(o)
o.class == self.class && o.state == state
end
def compress
TestCompressedTeam.new(
@name,
@players.map{|p| @exchange.check_in(p) }
)
end
def distribute_exchange(exchange)
@players.each{|p| p.distribute_exchange(exchange) }
@exchange = exchange
end
protected
def state
[@name, @players]
end
end
class TestPlayer
def initialize(name)
@name = name
end
attr_reader :name
def distribute_exchange(exchange)
@exchange = exchange
end
def ==(o)
o.class == self.class && o.state == state
end
protected
def state
[@name]
end
end
class TestPlayer
def initialize(name)
@name = name
end
attr_reader :name
def ==(o)
o.class == self.class && o.state == state
end
protected
def state
[@name]
end
end
class TestTeam
def initialize(name, players=[])
@name = name
@players = players
end
attr_reader :name, :players
def ==(o)
o.class == self.class && o.state == state
end
def distribute_exchange(exchange)
@players.each{|p| p.distribute_exchange(exchange) }
@exchange = exchange
end
protected
def state
[@name, @players]
end
end
class TestTeam
def initialize(name, players=[])
@name = name
@players = players
end
attr_reader :name, :players
def ==(o)
o.class == self.class && o.state == state
end
protected
def state
[@name, @players]
end
end
require 'digest'
class ValueRecord < ApplicationRecord
class LookupFailedException < Exception
end
class ValuePointer
def initialize(key)
@key = key
end
def distribute_exchange(exchange)
@exchange = exchange
end
def method_missing(meth, *args, &block)
@exchange.check_out(@key).send(meth, *args, &block)
end
end
def check_out(key)
@cache ||= {}
return @cache.fetch(key) if @cache.has_key?(key)
vr = ValueRecord.find(key)
if vr
return vr.value
else
raise LookupFailedException.new("unable to find key #{key}")
end
end
def check_in(val)
vr = ValueRecord.from_value(val)
vr.put!
ValuePointer.new(vr.key)
end
def compress
v_compressed = value.compress
ValueRecord.from_value(v_compressed)
end
def self.from_value(v)
serialized = Oj::dump(v, ignore: [ValueRecord])
ValueRecord.new(
key: Digest::SHA256.base64digest(serialized),
body: JSON.parse(serialized)
)
end
def value
v = Oj::load(body.to_json)
v.distribute_exchange(self)
v
end
def put!
save!
rescue ActiveRecord::RecordNotUnique => e
if e.message.include? "PG::UniqueViolation: ERROR: duplicate key value violates unique constraint \"value_records_pkey\""
return
else
raise e
end
end
end
require 'rails_helper'
require_relative '../fixtures/test_team.rb'
RSpec.describe ValueRecord, type: :model do
it "can be made to and from a PORO Value" do
v = TestTeam.new("Blue Team")
vr = ValueRecord.from_value(v)
vr.save!
vr_again = ValueRecord.find(vr.key)
expect(vr_again.value).to eq(v)
end
end
require 'rails_helper'
require_relative '../fixtures/test_player.rb'
require_relative '../fixtures/test_compressed_team.rb'
RSpec.describe ValueRecord, type: :model do
it "can compress values to value pointers" do
2.times do
name1 = "Player 1"*1000
name2 = "Player 2"*1000
v = TestCompressedTeam.new(
"Blue Team",
[
TestPlayer.new(name1),
TestPlayer.new(name2)
])
vr = ValueRecord.from_value(v)
vr.put!
vr_again = ValueRecord.find(vr.key)
vr_body_size = vr.body.to_json.size
vr_compressed = vr_again.compress
vr_compressed = vr_compressed.compress #test it's idempotent
vr_compressed.put!
vr_again = ValueRecord.find(vr_compressed.key)
vr_again_body_size = vr_again.body.to_json.size
v = vr_again.value
expect(v.name).to eq("Blue Team")
expect(v.players[0].name).to eq(name1)
expect(v.players[1].name).to eq(name2)
expect(vr_again_body_size).to be < vr_body_size
end
end
end
RSpec.describe ValueRecord, type: :model do
it "can dereference value pointers values" do
p2 = TestPlayer.new("Player 2")
vr2 = ValueRecord.from_value(p2)
vr2.save!
v = TestTeam.new(
"Blue Team",
[
TestPlayer.new("Player 1"),
ValueRecord::ValuePointer.new(vr2.key)
])
vr = ValueRecord.from_value(v)
vr.save!
vr_again = ValueRecord.find(vr.key)
v = vr_again.value
expect(v.name).to eq("Blue Team")
expect(v.players[0].name).to eq("Player 1")
expect(v.players[1].name).to eq("Player 2")
end
end
require 'digest'
class ValueRecord < ApplicationRecord
def self.from_value(v)
serialized = Oj::dump(v, ignore: [ValueRecord])
ValueRecord.new(
key: Digest::SHA256.base64digest(serialized),
body: JSON.parse(serialized)
)
end
def value
Oj::load(body.to_json)
end
end
require 'digest'
class ValueRecord < ApplicationRecord
class LookupFailedException < Exception
end
class ValuePointer
def initialize(key)
@key = key
end
def distribute_exchange(exchange)
@exchange = exchange
end
def method_missing(meth, *args, &block)
@exchange.check_out(@key).send(meth, *args, &block)
end
end
def check_out(key)
@cache ||= {}
return @cache.fetch(key) if @cache.has_key?(key)
vr = ValueRecord.find(key)
if vr
return vr.value
else
raise LookupFailedException.new("unable to find key #{key}")
end
end
def self.from_value(v)
serialized = Oj::dump(v, ignore: [ValueRecord])
ValueRecord.new(
key: Digest::SHA256.base64digest(serialized),
body: JSON.parse(serialized)
)
end
def value
v = Oj::load(body.to_json)
v.distribute_exchange(self)
v
end
end
require 'rails_helper'
require_relative '../fixtures/test_team.rb'
require_relative '../fixtures/test_player.rb'
RSpec.describe ValueRecord, type: :model do
it "can nest values" do
v = TestTeam.new(
"Blue Team",
[
TestPlayer.new("Player 1"),
TestPlayer.new("Player 2")
])
vr = ValueRecord.from_value(v)
vr.save!
vr_again = ValueRecord.find(vr.key)
expect(vr_again.value).to eq(v)
end
end
require 'rails_helper'
require_relative '../fixtures/test_team.rb'
require_relative '../fixtures/test_player.rb'
require_relative '../fixtures/test_compressed_team.rb'
RSpec.describe ValueRecord, type: :model do
it "can dereference value pointers values" do
p2 = TestPlayer.new("Player 2")
vr2 = ValueRecord.from_value(p2)
vr2.save!
v = TestTeam.new(
"Blue Team",
[
TestPlayer.new("Player 1"),
ValueRecord::ValuePointer.new(vr2.key)
])
vr = ValueRecord.from_value(v)
vr.save!
vr_again = ValueRecord.find(vr.key)
v = vr_again.value
expect(v.name).to eq("Blue Team")
expect(v.players[0].name).to eq("Player 1")
expect(v.players[1].name).to eq("Player 2")
end
it "can compress values to value pointers" do
2.times do
name1 = "Player 1"*1000
name2 = "Player 2"*1000
v = TestCompressedTeam.new(
"Blue Team",
[
TestPlayer.new(name1),
TestPlayer.new(name2)
])
vr = ValueRecord.from_value(v)
vr.put!
vr_again = ValueRecord.find(vr.key)
vr_body_size = vr.body.to_json.size
vr_compressed = vr_again.compress
vr_compressed = vr_compressed.compress #test it's idempotent
vr_compressed.put!
vr_again = ValueRecord.find(vr_compressed.key)
vr_again_body_size = vr_again.body.to_json.size
v = vr_again.value
expect(v.name).to eq("Blue Team")
expect(v.players[0].name).to eq(name1)
expect(v.players[1].name).to eq(name2)
expect(vr_again_body_size).to be < vr_body_size
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment