Skip to content

Instantly share code, notes, and snippets.

@opsb
Last active August 29, 2015 14:20
Show Gist options
  • Select an option

  • Save opsb/d1e9882b9be88af6d9f8 to your computer and use it in GitHub Desktop.

Select an option

Save opsb/d1e9882b9be88af6d9f8 to your computer and use it in GitHub Desktop.
ActiveRecordOrderedSet for redis
class Redis::ActiveRecordOrderedSet
include Enumerable
delegate :each, :to => :entries
def initialize(name, options={})
@set_name = name
@data_set = Redis::SortedSet.new(@set_name)
@updated_at_set = Redis::HashKey.new("meta_sorted_set::updated_at")
@storage_limit = options[:storage_limit]
end
def add(record)
@data_set.add(key_for_record(record), Time.zone.now.to_i)
prune!
touch!
end
alias_method :<<, :add
def delete(record)
@data_set.delete(key_for_record(record))
end
def updated_at
time = @updated_at_set[@set_name]
time && Time.at(time.to_i)
end
def length
@data_set.length
end
alias_method :size, :length
alias_method :count, :length
def top(count)
@data_set.revrange(0, count-1).map do |key|
record_for_key(key)
end.compact
end
def [](index)
key = @data_set.revrange(index, index).first
key && record_for_key(key)
end
def touch!
@updated_at_set[@set_name] = Time.zone.now.to_i
end
def count
@data_set.length
end
private
def entries
keys = @data_set.members.reverse
Content.children_for_keys(keys).compact
end
def key_for_record(record)
raise "Record was null" unless record
"#{record.class.to_s.underscore}-#{record.id}"
end
def record_for_key(key)
class_name, id = key.split("-")
record = class_name.singularize.classify.constantize.find_by_id(id)
end
def prune!
return unless @storage_limit
if count > @storage_limit
@data_set.remrangebyrank(0, count - 1 - @storage_limit)
end
end
end
require 'spec_helper'
describe Redis::ActiveRecordOrderedSet do
describe "empty set", unscoped: true do
let(:active_record_ordered_set){ Redis::ActiveRecordOrderedSet.new("tenant-test-trending-content") }
let(:post){ FactoryGirl.create(:post) }
let(:index){ 1 }
context "after adding an item" do
let(:now){ Time.now }
before do
Timecop.freeze(now)
active_record_ordered_set.add(post)
end
it "should have set updated_at" do
active_record_ordered_set.updated_at.to_i.should eq now.to_i
end
it "should include post in top items" do
active_record_ordered_set.top(1).first.should == post
end
it "should include the item during iteration" do
items = []
active_record_ordered_set.each do |item|
items << item
end
items.should include(post)
end
end
end
describe "limited to 5 entries", unscoped: true do
let(:limit){ 5 }
let(:active_record_ordered_set){ Redis::ActiveRecordOrderedSet.new("tenant-test-trending-content", storage_limit: limit) }
let(:items){ (1..10).map{ |n| FactoryGirl.create(:post) } }
context "after adding 10 items" do
before do
0.upto(9) do |n|
Timecop.freeze( n.minute.from_now ) do
active_record_ordered_set.add(items[n])
end
end
end
it "should only contain 5 items" do
active_record_ordered_set.count.should == 5
end
it "should have kept the 5 newest items" do
active_record_ordered_set.top(5).should == items[5..9].reverse
end
it "should include all posts for #to_a" do
active_record_ordered_set.to_a.should == items[5..9].reverse
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment