Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Version 3 proposal: Store optional member data in a single hash #26

Closed
wants to merge 3 commits into from

3 participants

@czarneckid
Owner

This has the benefit of not using a separate hash for every single
member's data. As is updated in the documentation, you can store
more data for a given member by, for example, encoding a Hash
in JSON. Not only will this save on Hash-splosion if using member
data in a leaderboard, it also means that when deleting a
leaderboard, we can also delete ALL of the member data at once. In
version 2.x, if using member data, you would have to go through and
delete the member data hashes individually. Yikes!

@czarneckid czarneckid Version 3 proposal: Store optional member data in a single hash
This has the benefit of not using a separate hash for every single
member's data. As is updated in the documentation, you can store
more data for a given member by, for example, encoding a Hash
in JSON. Not only will this save on Hash-splosion if using member
data in a leaderboard, it also means that when deleting a
leaderboard, we can also delete ALL of the member data at once. In
version 2.x, if using member data, you would have to go through and
delete the member data hashes individually. Yikes!
db1776e
@czarneckid
Owner

@jgadbois Any thoughts on this proposal for version 3 of the gem? I neglected at the time to consider the implications of the large # of hashes and having to cleanup member data in an easy way when you remove a leaderboard.

Makes sense, I didn't anticipate that either. I like passing a hash as the member data and having leaderboard conver it internally vs having to pass in a JSON string. Are you just trying to allow more flexibility in the type of member data?

czarneckid added some commits
@czarneckid czarneckid Version 3 proposal: Store optional member data in a single hash
This has the benefit of not using a separate hash for every single
member's data. As is updated in the documentation, you can store
more data for a given member by, for example, encoding a Hash
in JSON. Not only will this save on Hash-splosion if using member
data in a leaderboard, it also means that when deleting a
leaderboard, we can also delete ALL of the member data at once. In
version 2.x, if using member data, you would have to go through and
delete the member data hashes individually. Yikes!
ddc5bea
@czarneckid czarneckid Merge branch 'version-3-proposal-store-member-data-in-a-single-hash' …
…of github.com:agoragames/leaderboard into version-3-proposal-store-member-data-in-a-single-hash
3307a2b
@hypomodern
Owner

Have we benchmarked the two approaches?

@czarneckid
Owner

Benchmarked which approaches?

@hypomodern
Owner

You are proposing to change the present implementation. Is your proposed change slower or faster or "doesn't matter" than the original? It seems to me this is one area where performance might matter, since you'll often be retrieving or updating a bunch of member data in one unit of work.

@czarneckid
Owner

It will be faster since we are only ever retrieving one piece of data O(1) in this implementation from a Redis hash as opposed to retrieving all of the items from a hash O(n) in the previous implementation. Also, retrieving member data when pulling leader data is still optional in the call.

@czarneckid
Owner

This code has been integrated into the version 3 branch.

@czarneckid czarneckid closed this
This was referenced
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 16, 2012
  1. @czarneckid

    Version 3 proposal: Store optional member data in a single hash

    czarneckid authored
    This has the benefit of not using a separate hash for every single
    member's data. As is updated in the documentation, you can store
    more data for a given member by, for example, encoding a Hash
    in JSON. Not only will this save on Hash-splosion if using member
    data in a leaderboard, it also means that when deleting a
    leaderboard, we can also delete ALL of the member data at once. In
    version 2.x, if using member data, you would have to go through and
    delete the member data hashes individually. Yikes!
  2. @czarneckid

    Version 3 proposal: Store optional member data in a single hash

    czarneckid authored
    This has the benefit of not using a separate hash for every single
    member's data. As is updated in the documentation, you can store
    more data for a given member by, for example, encoding a Hash
    in JSON. Not only will this save on Hash-splosion if using member
    data in a leaderboard, it also means that when deleting a
    leaderboard, we can also delete ALL of the member data at once. In
    version 2.x, if using member data, you would have to go through and
    delete the member data hashes individually. Yikes!
  3. @czarneckid

    Merge branch 'version-3-proposal-store-member-data-in-a-single-hash' …

    czarneckid authored
    …of github.com:agoragames/leaderboard into version-3-proposal-store-member-data-in-a-single-hash
This page is out of date. Refresh to see the latest.
View
22 README.markdown
@@ -119,30 +119,34 @@ Get some information about your leaderboard:
=> 1
```
-The `rank_member` call will also accept an optional hash of member data that could
-be used to store other information about a given member in the leaderboard. This
-may be useful in situations where you are storing member IDs in the leaderboard and
-you want to be able to store a member name for display. Example:
+The `rank_member` call will also accept an optional string member data that could
+be used to store other information about a given member in the leaderboard. This
+may be useful in situations where you are storing member IDs in the leaderboard and
+you want to be able to store a member name for display. You could use JSON to
+encode a Hash of member data. Example:
```ruby
-highscore_lb.rank_member('84849292', 1, {'username' => 'member_name'})
+require 'json'
+highscore_lb.rank_member('84849292', 1, JSON.generate({'username' => 'member_name'})
```
You can retrieve, update and remove the optional member data using the
`member_data_for`, `update_member_data` and `remove_member_data` calls. Example:
```ruby
-highscore_lb.member_data_for('84849292')
+JSON.parse(highscore_lb.member_data_for('84849292'))
=> {"username"=>"member_name"}
-highscore_lb.update_member_data('84849292', {'last_updated' => Time.now, 'username' => 'updated_member_name'})
- => "OK"
-highscore_lb.member_data_for('84849292')
+highscore_lb.update_member_data('84849292', JSON.generate({'last_updated' => Time.now, 'username' => 'updated_member_name'}))
+ => "OK"
+JSON.parse(highscore_lb.member_data_for('84849292'))
=> {"username"=>"updated_member_name", "last_updated"=>"2012-06-09 09:11:06 -0400"}
highscore_lb.remove_member_data('84849292')
```
+If you delete the leaderboard, ALL of the member data is deleted as well.
+
Get some information about a specific member(s) in the leaderboard:
```ruby
View
26 lib/leaderboard.rb
@@ -103,7 +103,10 @@ def delete_leaderboard
#
# @param leaderboard_name [String] Name of the leaderboard.
def delete_leaderboard_named(leaderboard_name)
- @redis_connection.del(leaderboard_name)
+ @redis_connection.multi do |transaction|
+ transaction.del(leaderboard_name)
+ transaction.del(member_data_key(leaderboard_name))
+ end
end
# Rank a member in the leaderboard.
@@ -124,9 +127,7 @@ def rank_member(member, score, member_data = nil)
def rank_member_in(leaderboard_name, member, score, member_data)
@redis_connection.multi do |transaction|
transaction.zadd(leaderboard_name, score, member)
- if member_data
- transaction.hmset(member_data_key(leaderboard_name, member), *member_data.to_a.flatten)
- end
+ transaction.hset(member_data_key(leaderboard_name), member, member_data) if member_data
end
end
@@ -146,7 +147,7 @@ def member_data_for(member)
#
# @return Hash of optional member data.
def member_data_for_in(leaderboard_name, member)
- @redis_connection.hgetall(member_data_key(leaderboard_name, member))
+ @redis_connection.hget(member_data_key(leaderboard_name), member)
end
# Update the optional member data for a given member in the leaderboard.
@@ -163,7 +164,7 @@ def update_member_data(member, member_data)
# @param member [String] Member name.
# @param member_data [Hash] Optional member data.
def update_member_data_in(leaderboard_name, member, member_data)
- @redis_connection.hmset(member_data_key(leaderboard_name, member), *member_data.to_a.flatten)
+ @redis_connection.hset(member_data_key(leaderboard_name), member, member_data)
end
# Remove the optional member data for a given member in the leaderboard.
@@ -178,7 +179,7 @@ def remove_member_data(member)
# @param leaderboard_name [String] Name of the leaderboard.
# @param member [String] Member name.
def remove_member_data_in(leaderboard_name, member)
- @redis_connection.del(member_data_key(leaderboard_name, member))
+ @redis_connection.hdel(member_data_key(leaderboard_name), member)
end
# Rank an array of members in the leaderboard.
@@ -218,7 +219,7 @@ def remove_member(member)
def remove_member_from(leaderboard_name, member)
@redis_connection.multi do |transaction|
transaction.zrem(leaderboard_name, member)
- transaction.del(member_data_key(leaderboard_name, member))
+ transaction.del(member_data_key(leaderboard_name), member)
end
end
@@ -815,11 +816,10 @@ def intersect_leaderboards(destination, keys, options = {:aggregate => :sum})
# Key for retrieving optional member data.
#
# @param leaderboard_name [String] Name of the leaderboard.
- # @param member [String] Member name.
- #
- # @return a key in the form of +leaderboard_name:data:member+
- def member_data_key(leaderboard_name, member)
- "#{leaderboard_name}:member_data:#{member}"
+ #
+ # @return a key in the form of +leaderboard_name:member_data+
+ def member_data_key(leaderboard_name)
+ "#{leaderboard_name}:member_data"
end
# Validate and return the page size. Returns the +DEFAULT_PAGE_SIZE+ if the page size is less than 1.
View
38 spec/leaderboard_spec.rb
@@ -40,8 +40,10 @@
rank_members_in_leaderboard
@redis_connection.exists('name').should be_true
+ @redis_connection.exists('name:member_data').should be_true
@leaderboard.delete_leaderboard
@redis_connection.exists('name').should be_false
+ @redis_connection.exists('name:member_data').should be_false
end
it 'should allow you to pass in an existing redis connection in the initializer' do
@@ -170,11 +172,11 @@
members = @leaderboard.members_from_score_range(10, 15, {:with_scores => true, :with_rank => true, :with_member_data => true})
- member_15 = {:member => 'member_15', :rank => 11, :score => 15.0, :member_data => {'member_name' => 'Leaderboard member 15'}}
- members[0].should eql(member_15)
+ member_15 = {:member => 'member_15', :rank => 11, :score => 15.0, :member_data => {:member_name => 'Leaderboard member 15'}.to_s}
+ members[0].should == member_15
- member_10 = {:member => 'member_10', :rank => 16, :score => 10.0, :member_data => {'member_name' => 'Leaderboard member 10'}}
- members[5].should eql(member_10)
+ member_10 = {:member => 'member_10', :rank => 16, :score => 10.0, :member_data => {:member_name => 'Leaderboard member 10'}.to_s}
+ members[5].should == member_10
end
it 'should allow you to retrieve leaders without scores and ranks' do
@@ -196,34 +198,34 @@
@leaderboard.total_members.should be(Leaderboard::DEFAULT_PAGE_SIZE)
leaders = @leaderboard.leaders(1, {:with_scores => false, :with_rank => false, :with_member_data => true})
- member_25 = {:member => 'member_25', :member_data => { "member_name" => "Leaderboard member 25" }}
- leaders[0].should eql(member_25)
-
- member_1 = {:member => 'member_1', :member_data => { "member_name" => "Leaderboard member 1" }}
- leaders[24].should eql(member_1)
+ member_25 = {:member => 'member_25', :member_data => { :member_name => "Leaderboard member 25" }.to_s }
+ leaders[0].should == member_25
+
+ member_1 = {:member => 'member_1', :member_data => { :member_name => "Leaderboard member 1" }.to_s }
+ leaders[24].should == member_1
end
it 'should allow you to retrieve optional member data' do
@leaderboard.rank_member('member_id', 1, {'username' => 'member_name', 'other_data_key' => 'other_data_value'})
- @leaderboard.member_data_for('unknown_member').should eql({})
- @leaderboard.member_data_for('member_id').should eql({'username' => 'member_name', 'other_data_key' => 'other_data_value'})
+ @leaderboard.member_data_for('unknown_member').should be_nil
+ @leaderboard.member_data_for('member_id').should == {'username' => 'member_name', 'other_data_key' => 'other_data_value'}.to_s
end
it 'should allow you to update optional member data' do
@leaderboard.rank_member('member_id', 1, {'username' => 'member_name'})
- @leaderboard.member_data_for('member_id').should eql({'username' => 'member_name'})
- @leaderboard.update_member_data('member_id', {'other_data_key' => 'other_data_value'})
- @leaderboard.member_data_for('member_id').should eql({'username' => 'member_name', 'other_data_key' => 'other_data_value'})
+ @leaderboard.member_data_for('member_id').should == {'username' => 'member_name'}.to_s
+ @leaderboard.update_member_data('member_id', {'username' => 'member_name', 'other_data_key' => 'other_data_value'})
+ @leaderboard.member_data_for('member_id').should == {'username' => 'member_name', 'other_data_key' => 'other_data_value'}.to_s
end
it 'should allow you to remove optional member data' do
@leaderboard.rank_member('member_id', 1, {'username' => 'member_name'})
- @leaderboard.member_data_for('member_id').should eql({'username' => 'member_name'})
+ @leaderboard.member_data_for('member_id').should == {'username' => 'member_name'}.to_s
@leaderboard.remove_member_data('member_id')
- @leaderboard.member_data_for('member_id').should eql({})
+ @leaderboard.member_data_for('member_id').should be_nil
end
it 'should allow you to call leaders with various options that respect the defaults for the options not passed in' do
@@ -268,8 +270,8 @@
@leaderboard.member_at(26)[:rank].should eql(26)
@leaderboard.member_at(50)[:rank].should eql(50)
@leaderboard.member_at(51).should be_nil
- @leaderboard.member_at(1, :with_member_data => true)[:member_data].should eql({'member_name' => 'Leaderboard member 50'})
- @leaderboard.member_at(1, :use_zero_index_for_rank => true)[:rank].should eql(0)
+ @leaderboard.member_at(1, :with_member_data => true)[:member_data].should == {:member_name => 'Leaderboard member 50'}.to_s
+ @leaderboard.member_at(1, :use_zero_index_for_rank => true)[:rank].should == 0
end
it 'should return the correct information when calling around_me' do
View
12 spec/reverse_leaderboard_spec.rb
@@ -60,11 +60,11 @@
members = @leaderboard.members_from_score_range(10, 15, {:with_scores => true, :with_rank => true, :with_member_data => true})
- member_10 = {:member => 'member_10', :rank => 10, :score => 10.0, :member_data => {'member_name' => 'Leaderboard member 10'}}
- members[0].should eql(member_10)
+ member_10 = {:member => 'member_10', :rank => 10, :score => 10.0, :member_data => {:member_name => 'Leaderboard member 10'}.to_s}
+ members[0].should == member_10
- member_15 = {:member => 'member_15', :rank => 15, :score => 15.0, :member_data => {'member_name' => 'Leaderboard member 15'}}
- members[5].should eql(member_15)
+ member_15 = {:member => 'member_15', :rank => 15, :score => 15.0, :member_data => {:member_name => 'Leaderboard member 15'}.to_s}
+ members[5].should == member_15
end
it 'should allow you to retrieve leaders without scores and ranks' do
@@ -122,8 +122,8 @@
@leaderboard.member_at(26)[:rank].should eql(26)
@leaderboard.member_at(50)[:rank].should eql(50)
@leaderboard.member_at(51).should be_nil
- @leaderboard.member_at(1, :with_member_data => true)[:member_data].should eql({'member_name' => 'Leaderboard member 1'})
- @leaderboard.member_at(1, :use_zero_index_for_rank => true)[:rank].should eql(0)
+ @leaderboard.member_at(1, :with_member_data => true)[:member_data].should == {:member_name => 'Leaderboard member 1'}.to_s
+ @leaderboard.member_at(1, :use_zero_index_for_rank => true)[:rank].should == 0
end
it 'should return the correct information when calling around_me' do
Something went wrong with that request. Please try again.