sohm 0.9.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/sohm.rb +20 -219
- data/sohm.gemspec +1 -1
- data/test/indices.rb +0 -10
- data/test/json.rb +0 -10
- data/test/model.rb +0 -16
- data/test/set.rb +0 -13
- metadata +2 -14
- data/benchmarks/common.rb +0 -33
- data/benchmarks/create.rb +0 -21
- data/benchmarks/delete.rb +0 -13
- data/examples/activity-feed.rb +0 -162
- data/examples/chaining.rb +0 -162
- data/examples/json-hash.rb +0 -75
- data/examples/one-to-many.rb +0 -124
- data/examples/philosophy.rb +0 -137
- data/examples/redis-logging.txt +0 -179
- data/examples/slug.rb +0 -149
- data/examples/tagging.rb +0 -237
- data/test/filtering.rb +0 -182
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2d4f419606a66335e7a44d8440732b4179a068f
|
4
|
+
data.tar.gz: 6565464772af7ad624fb92268cac9ede60f70b44
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6336ecb58d5112f896f673730c58b9810731342bf88183bebae05cef484a7eb377a9ffff95978211122889d6be61d0820bebc2d6c134d8c97a007e9e501aaf3d
|
7
|
+
data.tar.gz: f6c0592a5f52e04352121bc9975b30b45ac13c6e677a4b27a43f1032df8bc1c92aa1123d83e03ca7f41a269c02bd50679e44e675df8f54bdc1619cee2f9cb370
|
data/lib/sohm.rb
CHANGED
@@ -40,6 +40,7 @@ module Sohm
|
|
40
40
|
class MissingID < Error; end
|
41
41
|
class IndexNotFound < Error; end
|
42
42
|
class CasViolation < Error; end
|
43
|
+
class NotSupported < Error; end
|
43
44
|
|
44
45
|
# Instead of monkey patching Kernel or trying to be clever, it's
|
45
46
|
# best to confine all the helper methods in a Utils module.
|
@@ -416,61 +417,6 @@ module Sohm
|
|
416
417
|
@model = model
|
417
418
|
end
|
418
419
|
|
419
|
-
# Chain new fiters on an existing set.
|
420
|
-
#
|
421
|
-
# Example:
|
422
|
-
#
|
423
|
-
# set = User.find(:name => "John")
|
424
|
-
# set.find(:age => 30)
|
425
|
-
#
|
426
|
-
def find(dict)
|
427
|
-
MultiSet.new(
|
428
|
-
namespace, model, Command[:sinterstore, key, *model.filters(dict)]
|
429
|
-
)
|
430
|
-
end
|
431
|
-
|
432
|
-
# Reduce the set using any number of filters.
|
433
|
-
#
|
434
|
-
# Example:
|
435
|
-
#
|
436
|
-
# set = User.find(:name => "John")
|
437
|
-
# set.except(:country => "US")
|
438
|
-
#
|
439
|
-
# # You can also do it in one line.
|
440
|
-
# User.find(:name => "John").except(:country => "US")
|
441
|
-
#
|
442
|
-
def except(dict)
|
443
|
-
MultiSet.new(namespace, model, key).except(dict)
|
444
|
-
end
|
445
|
-
|
446
|
-
# Perform an intersection between the existent set and
|
447
|
-
# the new set created by the union of the passed filters.
|
448
|
-
#
|
449
|
-
# Example:
|
450
|
-
#
|
451
|
-
# set = User.find(:status => "active")
|
452
|
-
# set.combine(:name => ["John", "Jane"])
|
453
|
-
#
|
454
|
-
# # The result will include all users with active status
|
455
|
-
# # and with names "John" or "Jane".
|
456
|
-
def combine(dict)
|
457
|
-
MultiSet.new(namespace, model, key).combine(dict)
|
458
|
-
end
|
459
|
-
|
460
|
-
# Do a union to the existing set using any number of filters.
|
461
|
-
#
|
462
|
-
# Example:
|
463
|
-
#
|
464
|
-
# set = User.find(:name => "John")
|
465
|
-
# set.union(:name => "Jane")
|
466
|
-
#
|
467
|
-
# # You can also do it in one line.
|
468
|
-
# User.find(:name => "John").union(:name => "Jane")
|
469
|
-
#
|
470
|
-
def union(dict)
|
471
|
-
MultiSet.new(namespace, model, key).union(dict)
|
472
|
-
end
|
473
|
-
|
474
420
|
private
|
475
421
|
def execute
|
476
422
|
yield key
|
@@ -511,128 +457,6 @@ module Sohm
|
|
511
457
|
end
|
512
458
|
end
|
513
459
|
|
514
|
-
# Anytime you filter a set with more than one requirement, you
|
515
|
-
# internally use a `MultiSet`. `MultiSet` is a bit slower than just
|
516
|
-
# a `Set` because it has to `SINTERSTORE` all the keys prior to
|
517
|
-
# retrieving the members, size, etc.
|
518
|
-
#
|
519
|
-
# Example:
|
520
|
-
#
|
521
|
-
# User.all.kind_of?(Sohm::Set)
|
522
|
-
# # => true
|
523
|
-
#
|
524
|
-
# User.find(:name => "John").kind_of?(Sohm::Set)
|
525
|
-
# # => true
|
526
|
-
#
|
527
|
-
# User.find(:name => "John", :age => 30).kind_of?(Sohm::MultiSet)
|
528
|
-
# # => true
|
529
|
-
#
|
530
|
-
class MultiSet < BasicSet
|
531
|
-
attr :namespace
|
532
|
-
attr :model
|
533
|
-
attr :command
|
534
|
-
|
535
|
-
def initialize(namespace, model, command)
|
536
|
-
@namespace = namespace
|
537
|
-
@model = model
|
538
|
-
@command = command
|
539
|
-
end
|
540
|
-
|
541
|
-
# Chain new fiters on an existing set.
|
542
|
-
#
|
543
|
-
# Example:
|
544
|
-
#
|
545
|
-
# set = User.find(:name => "John", :age => 30)
|
546
|
-
# set.find(:status => 'pending')
|
547
|
-
#
|
548
|
-
def find(dict)
|
549
|
-
MultiSet.new(
|
550
|
-
namespace, model, Command[:sinterstore, command, intersected(dict)]
|
551
|
-
)
|
552
|
-
end
|
553
|
-
|
554
|
-
# Reduce the set using any number of filters.
|
555
|
-
#
|
556
|
-
# Example:
|
557
|
-
#
|
558
|
-
# set = User.find(:name => "John")
|
559
|
-
# set.except(:country => "US")
|
560
|
-
#
|
561
|
-
# # You can also do it in one line.
|
562
|
-
# User.find(:name => "John").except(:country => "US")
|
563
|
-
#
|
564
|
-
def except(dict)
|
565
|
-
MultiSet.new(
|
566
|
-
namespace, model, Command[:sdiffstore, command, unioned(dict)]
|
567
|
-
)
|
568
|
-
end
|
569
|
-
|
570
|
-
# Perform an intersection between the existent set and
|
571
|
-
# the new set created by the union of the passed filters.
|
572
|
-
#
|
573
|
-
# Example:
|
574
|
-
#
|
575
|
-
# set = User.find(:status => "active")
|
576
|
-
# set.combine(:name => ["John", "Jane"])
|
577
|
-
#
|
578
|
-
# # The result will include all users with active status
|
579
|
-
# # and with names "John" or "Jane".
|
580
|
-
def combine(dict)
|
581
|
-
MultiSet.new(
|
582
|
-
namespace, model, Command[:sinterstore, command, unioned(dict)]
|
583
|
-
)
|
584
|
-
end
|
585
|
-
|
586
|
-
# Do a union to the existing set using any number of filters.
|
587
|
-
#
|
588
|
-
# Example:
|
589
|
-
#
|
590
|
-
# set = User.find(:name => "John")
|
591
|
-
# set.union(:name => "Jane")
|
592
|
-
#
|
593
|
-
# # You can also do it in one line.
|
594
|
-
# User.find(:name => "John").union(:name => "Jane")
|
595
|
-
#
|
596
|
-
def union(dict)
|
597
|
-
MultiSet.new(
|
598
|
-
namespace, model, Command[:sunionstore, command, intersected(dict)]
|
599
|
-
)
|
600
|
-
end
|
601
|
-
|
602
|
-
private
|
603
|
-
def redis
|
604
|
-
model.redis
|
605
|
-
end
|
606
|
-
|
607
|
-
def intersected(dict)
|
608
|
-
Command[:sinterstore, *model.filters(dict)]
|
609
|
-
end
|
610
|
-
|
611
|
-
def unioned(dict)
|
612
|
-
Command[:sunionstore, *model.filters(dict)]
|
613
|
-
end
|
614
|
-
|
615
|
-
def execute
|
616
|
-
# namespace[:tmp] is where all the temp keys should be stored in.
|
617
|
-
# redis will be where all the commands are executed against.
|
618
|
-
response = command.call(namespace[:tmp], redis)
|
619
|
-
|
620
|
-
begin
|
621
|
-
|
622
|
-
# At this point, we have the final aggregated set, which we yield
|
623
|
-
# to the caller. the caller can do all the normal set operations,
|
624
|
-
# i.e. SCARD, SMEMBERS, etc.
|
625
|
-
yield response
|
626
|
-
|
627
|
-
ensure
|
628
|
-
|
629
|
-
# We have to make sure we clean up the temporary keys to avoid
|
630
|
-
# memory leaks and the unintended explosion of memory usage.
|
631
|
-
command.clean
|
632
|
-
end
|
633
|
-
end
|
634
|
-
end
|
635
|
-
|
636
460
|
# The base class for all your models. In order to better understand
|
637
461
|
# it, here is a semi-realtime explanation of the details involved
|
638
462
|
# when creating a User instance.
|
@@ -655,33 +479,6 @@ module Sohm
|
|
655
479
|
# u.incr :points
|
656
480
|
# u.posts.add(Post.create)
|
657
481
|
#
|
658
|
-
# When you execute `User.create(...)`, you run the following Redis
|
659
|
-
# commands:
|
660
|
-
#
|
661
|
-
# # Generate an ID
|
662
|
-
# INCR User:id
|
663
|
-
#
|
664
|
-
# # Add the newly generated ID, (let's assume the ID is 1).
|
665
|
-
# SADD User:all 1
|
666
|
-
#
|
667
|
-
# # Store the unique index
|
668
|
-
# HSET User:uniques:email foo@bar.com 1
|
669
|
-
#
|
670
|
-
# # Store the name index
|
671
|
-
# SADD User:indices:name:John 1
|
672
|
-
#
|
673
|
-
# # Store the HASH
|
674
|
-
# HMSET User:1 name John email foo@bar.com
|
675
|
-
#
|
676
|
-
# Next we increment points:
|
677
|
-
#
|
678
|
-
# HINCR User:1:_counters points 1
|
679
|
-
#
|
680
|
-
# And then we add a Post to the `posts` set.
|
681
|
-
# (For brevity, let's assume the Post created has an ID of 1).
|
682
|
-
#
|
683
|
-
# SADD User:1:posts 1
|
684
|
-
#
|
685
482
|
class Model
|
686
483
|
def self.redis=(redis)
|
687
484
|
@redis = redis
|
@@ -788,16 +585,16 @@ module Sohm
|
|
788
585
|
# User.find(:tag => "python").include?(u)
|
789
586
|
# # => true
|
790
587
|
#
|
791
|
-
#
|
792
|
-
#
|
793
|
-
#
|
588
|
+
# Due to restrictions in Codis, we only support single-index query.
|
589
|
+
# If you want to query based on multiple fields, you can make an
|
590
|
+
# index based on all the fields.
|
794
591
|
def self.find(dict)
|
795
592
|
keys = filters(dict)
|
796
593
|
|
797
594
|
if keys.size == 1
|
798
595
|
Sohm::Set.new(keys.first, key, self)
|
799
596
|
else
|
800
|
-
|
597
|
+
raise NotSupported
|
801
598
|
end
|
802
599
|
end
|
803
600
|
|
@@ -1101,17 +898,9 @@ module Sohm
|
|
1101
898
|
# Access the ID used to store this model. The ID is used together
|
1102
899
|
# with the name of the class in order to form the Redis key.
|
1103
900
|
#
|
1104
|
-
#
|
1105
|
-
#
|
1106
|
-
#
|
1107
|
-
#
|
1108
|
-
# u = User.create
|
1109
|
-
# u.id
|
1110
|
-
# # => 1
|
1111
|
-
#
|
1112
|
-
# u.key
|
1113
|
-
# # => User:1
|
1114
|
-
#
|
901
|
+
# Different from ohm, id must be provided by the user in sohm,
|
902
|
+
# if you want to use auto-generated id, you can include Sohm::AutoId
|
903
|
+
# module.
|
1115
904
|
def id
|
1116
905
|
raise MissingID if not defined?(@id)
|
1117
906
|
@id
|
@@ -1207,6 +996,18 @@ module Sohm
|
|
1207
996
|
@serial_attributes
|
1208
997
|
end
|
1209
998
|
|
999
|
+
def counters
|
1000
|
+
hash = {}
|
1001
|
+
self.class.counters.each do |name|
|
1002
|
+
hash[name] = 0
|
1003
|
+
end
|
1004
|
+
return hash if new?
|
1005
|
+
redis.call("HGETALL", key[:_counters]).each_slice(2).each do |pair|
|
1006
|
+
hash[pair[0].to_sym] = pair[1].to_i
|
1007
|
+
end
|
1008
|
+
hash
|
1009
|
+
end
|
1010
|
+
|
1210
1011
|
# Export the ID of the model. The approach of Ohm is to
|
1211
1012
|
# whitelist public attributes, as opposed to exporting each
|
1212
1013
|
# (possibly sensitive) attribute.
|
data/sohm.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "sohm"
|
3
|
-
s.version = "0.
|
3
|
+
s.version = "0.10.0"
|
4
4
|
s.summary = %{Slim ohm for twemproxy-like system}
|
5
5
|
s.description = %Q{Slim ohm is a forked ohm that works with twemproxy-like redis system, only a limited set of features in ohm is supported}
|
6
6
|
s.authors = ["Xuejie Xiao"]
|
data/test/indices.rb
CHANGED
@@ -56,16 +56,6 @@ test "avoid intersections with the all collection" do
|
|
56
56
|
assert_equal "User:_indices:email:foo", User.find(:email => "foo").key
|
57
57
|
end
|
58
58
|
|
59
|
-
test "cleanup the temporary key after use" do
|
60
|
-
assert User.find(:email => "foo", :activation_code => "bar").to_a
|
61
|
-
|
62
|
-
assert Sohm.redis.call("KEYS", "User:temp:*").empty?
|
63
|
-
end
|
64
|
-
|
65
|
-
test "allow multiple chained finds" do
|
66
|
-
assert 1 == User.find(:email => "foo").find(:activation_code => "bar").find(:update => "baz").size
|
67
|
-
end
|
68
|
-
|
69
59
|
test "return nil if no results are found" do
|
70
60
|
assert User.find(:email => "foobar").empty?
|
71
61
|
assert nil == User.find(:email => "foobar").first
|
data/test/json.rb
CHANGED
@@ -45,16 +45,6 @@ test "exports a set to json" do
|
|
45
45
|
assert_equal expected, Programmer.all.to_json
|
46
46
|
end
|
47
47
|
|
48
|
-
test "exports a multiset to json" do
|
49
|
-
Programmer.create(language: "Ruby")
|
50
|
-
Programmer.create(language: "Python")
|
51
|
-
|
52
|
-
expected = [{ id: "1", language: "Ruby" }, { id: "2", language: "Python"}].to_json
|
53
|
-
result = Programmer.find(language: "Ruby").union(language: "Python").to_json
|
54
|
-
|
55
|
-
assert_equal expected, result
|
56
|
-
end
|
57
|
-
|
58
48
|
test "exports a list to json" do
|
59
49
|
venue = Venue.create(name: "Foo")
|
60
50
|
|
data/test/model.rb
CHANGED
@@ -410,12 +410,6 @@ test "finding by one entry in the enumerable" do |entry|
|
|
410
410
|
assert Entry.find(:tag => "baz").include?(entry)
|
411
411
|
end
|
412
412
|
|
413
|
-
test "finding by multiple entries in the enumerable" do |entry|
|
414
|
-
assert Entry.find(:tag => ["foo", "bar"]).include?(entry)
|
415
|
-
assert Entry.find(:tag => ["bar", "baz"]).include?(entry)
|
416
|
-
assert Entry.find(:tag => ["baz", "oof"]).empty?
|
417
|
-
end
|
418
|
-
|
419
413
|
# Attributes of type Set
|
420
414
|
setup do
|
421
415
|
@person1 = Person.create(:name => "Albert")
|
@@ -426,16 +420,6 @@ setup do
|
|
426
420
|
@event.name = "Ruby Tuesday"
|
427
421
|
end
|
428
422
|
|
429
|
-
test "filter elements" do
|
430
|
-
@event.save
|
431
|
-
@event.attendees.add(@person1)
|
432
|
-
@event.attendees.add(@person2)
|
433
|
-
|
434
|
-
assert [@person1] == @event.attendees.find(:initial => "A").to_a
|
435
|
-
assert [@person2] == @event.attendees.find(:initial => "B").to_a
|
436
|
-
assert [] == @event.attendees.find(:initial => "Z").to_a
|
437
|
-
end
|
438
|
-
|
439
423
|
test "delete elements" do
|
440
424
|
@event.save
|
441
425
|
@event.attendees.add(@person1)
|
data/test/set.rb
CHANGED
@@ -26,16 +26,3 @@ test '#exists? returns true if the given id is included in the set' do
|
|
26
26
|
|
27
27
|
assert user.posts.exists?(post.id)
|
28
28
|
end
|
29
|
-
|
30
|
-
test "#ids returns an array with the ids" do
|
31
|
-
user_ids = [
|
32
|
-
User.create(name: "John").id,
|
33
|
-
User.create(name: "Jane").id
|
34
|
-
]
|
35
|
-
|
36
|
-
assert_equal user_ids, User.all.ids
|
37
|
-
|
38
|
-
result = User.find(name: "John").union(name: "Jane")
|
39
|
-
|
40
|
-
assert_equal user_ids, result.ids
|
41
|
-
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sohm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xuejie Xiao
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-06-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redic
|
@@ -81,17 +81,6 @@ files:
|
|
81
81
|
- CHANGELOG.md
|
82
82
|
- LICENSE
|
83
83
|
- README.md
|
84
|
-
- benchmarks/common.rb
|
85
|
-
- benchmarks/create.rb
|
86
|
-
- benchmarks/delete.rb
|
87
|
-
- examples/activity-feed.rb
|
88
|
-
- examples/chaining.rb
|
89
|
-
- examples/json-hash.rb
|
90
|
-
- examples/one-to-many.rb
|
91
|
-
- examples/philosophy.rb
|
92
|
-
- examples/redis-logging.txt
|
93
|
-
- examples/slug.rb
|
94
|
-
- examples/tagging.rb
|
95
84
|
- lib/sohm.rb
|
96
85
|
- lib/sohm/auto_id.rb
|
97
86
|
- lib/sohm/command.rb
|
@@ -106,7 +95,6 @@ files:
|
|
106
95
|
- test/core.rb
|
107
96
|
- test/counters.rb
|
108
97
|
- test/enumerable.rb
|
109
|
-
- test/filtering.rb
|
110
98
|
- test/hash_key.rb
|
111
99
|
- test/helper.rb
|
112
100
|
- test/indices.rb
|
data/benchmarks/common.rb
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
require "bench"
|
2
|
-
require_relative "../lib/ohm"
|
3
|
-
|
4
|
-
Ohm.redis = Redic.new("redis://127.0.0.1:6379/15")
|
5
|
-
Ohm.flush
|
6
|
-
|
7
|
-
class Event < Ohm::Model
|
8
|
-
attribute :name
|
9
|
-
attribute :location
|
10
|
-
|
11
|
-
index :name
|
12
|
-
index :location
|
13
|
-
|
14
|
-
def validate
|
15
|
-
assert_present :name
|
16
|
-
assert_present :location
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
class Sequence
|
21
|
-
def initialize
|
22
|
-
@value = 0
|
23
|
-
end
|
24
|
-
|
25
|
-
def succ!
|
26
|
-
Thread.exclusive { @value += 1 }
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.[](name)
|
30
|
-
@@sequences ||= Hash.new { |hash, key| hash[key] = Sequence.new }
|
31
|
-
@@sequences[name]
|
32
|
-
end
|
33
|
-
end
|
data/benchmarks/create.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
require_relative "common"
|
2
|
-
|
3
|
-
benchmark "Create Events" do
|
4
|
-
i = Sequence[:events].succ!
|
5
|
-
|
6
|
-
Event.create(:name => "Redis Meetup #{i}", :location => "London #{i}")
|
7
|
-
end
|
8
|
-
|
9
|
-
benchmark "Find by indexed attribute" do
|
10
|
-
Event.find(:name => "Redis Meetup 1").first
|
11
|
-
end
|
12
|
-
|
13
|
-
benchmark "Mass update" do
|
14
|
-
Event[1].update(:name => "Redis Meetup II")
|
15
|
-
end
|
16
|
-
|
17
|
-
benchmark "Load events" do
|
18
|
-
Event[1].name
|
19
|
-
end
|
20
|
-
|
21
|
-
run 5000
|
data/benchmarks/delete.rb
DELETED
data/examples/activity-feed.rb
DELETED
@@ -1,162 +0,0 @@
|
|
1
|
-
### Building an activity feed
|
2
|
-
|
3
|
-
#### Common solutions using a relational design
|
4
|
-
|
5
|
-
# When faced with this application requirement, the most common approach by
|
6
|
-
# far have been to create an *activities* table, and rows in this table would
|
7
|
-
# reference a *user*. Activities would typically be generated for each
|
8
|
-
# follower (or friend) when a certain user performs an action, like posting a
|
9
|
-
# new status update.
|
10
|
-
|
11
|
-
#### Problems
|
12
|
-
|
13
|
-
# The biggest issue with this design, is that the *activities* table will
|
14
|
-
# quickly get very huge, at which point you would need to shard it on
|
15
|
-
# *user_id*. Also, inserting thousands of entries per second would quickly
|
16
|
-
# bring your database to its knees.
|
17
|
-
|
18
|
-
#### Ohm Solution
|
19
|
-
|
20
|
-
# As always we need to require `Ohm`.
|
21
|
-
require "ohm"
|
22
|
-
|
23
|
-
# We create a `User` class, with a `set` for all the other users he
|
24
|
-
# would be `following`, and another `set` for all his `followers`.
|
25
|
-
class User < Ohm::Model
|
26
|
-
set :followers, User
|
27
|
-
set :following, User
|
28
|
-
|
29
|
-
# Because a `User` literally has a `list` of activities, using a Redis
|
30
|
-
# `list` to model the activities would be a good choice. We default to
|
31
|
-
# getting the first 100 activities, and use
|
32
|
-
# [lrange](http://code.google.com/p/redis/wiki/LrangeCommand) directly.
|
33
|
-
def activities(start = 0, limit = 100)
|
34
|
-
key[:activities].lrange(start, start + limit)
|
35
|
-
end
|
36
|
-
|
37
|
-
# Broadcasting a message to all the `followers` of a user would simply
|
38
|
-
# be prepending the message for each if his `followers`. We also use
|
39
|
-
# the Redis command
|
40
|
-
# [lpush](http://code.google.com/p/redis/wiki/RpushCommand) directly.
|
41
|
-
def broadcast(str)
|
42
|
-
followers.each do |user|
|
43
|
-
user.key[:activities].lpush(str)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# Given that *Jane* wants to follow *John*, we simply do the following
|
48
|
-
# steps:
|
49
|
-
#
|
50
|
-
# 1. *John* is added to *Jane*'s `following` list.
|
51
|
-
# 2. *Jane* is added to *John*'s `followers` list.
|
52
|
-
def follow(other)
|
53
|
-
following << other
|
54
|
-
other.followers << self
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
|
59
|
-
#### Testing
|
60
|
-
|
61
|
-
# We'll use cutest for our testing framework.
|
62
|
-
require "cutest"
|
63
|
-
|
64
|
-
# The database is flushed before each test.
|
65
|
-
prepare { Ohm.flush }
|
66
|
-
|
67
|
-
# We define two users, `john` and `jane`, and yield them so all
|
68
|
-
# other tests are given access to these 2 users.
|
69
|
-
setup do
|
70
|
-
john = User.create
|
71
|
-
jane = User.create
|
72
|
-
|
73
|
-
[john, jane]
|
74
|
-
end
|
75
|
-
|
76
|
-
# Let's verify our model for `follow`. When `jane` follows `john`,
|
77
|
-
# the following conditions should hold:
|
78
|
-
#
|
79
|
-
# 1. The followers list of `john` is comprised *only* of `jane`.
|
80
|
-
# 2. The list of users `jane` is following is comprised *only* of `john`.
|
81
|
-
test "jane following john" do |john, jane|
|
82
|
-
jane.follow(john)
|
83
|
-
|
84
|
-
assert [john] == jane.following.to_a
|
85
|
-
assert [jane] == john.followers.to_a
|
86
|
-
end
|
87
|
-
|
88
|
-
# Broadcasting a message should simply notify all the followers of the
|
89
|
-
# `broadcaster`.
|
90
|
-
test "john broadcasting a message" do |john, jane|
|
91
|
-
jane.follow(john)
|
92
|
-
john.broadcast("Learning about Redis and Ohm")
|
93
|
-
|
94
|
-
assert jane.activities.include?("Learning about Redis and Ohm")
|
95
|
-
end
|
96
|
-
|
97
|
-
#### Total Denormalization: Adding HTML
|
98
|
-
|
99
|
-
# This may be a real edge case design decision, but for some scenarios this
|
100
|
-
# may work. The beauty of this solution is that you only have to generate the
|
101
|
-
# output once, and successive refreshes of the end user will help you save
|
102
|
-
# some CPU cycles.
|
103
|
-
#
|
104
|
-
# This example of course assumes that the code that generates this does all
|
105
|
-
# the conditional checks (possibly changing the point of view like *Me:*
|
106
|
-
# instead of *John says:*).
|
107
|
-
test "broadcasting the html directly" do |john, jane|
|
108
|
-
jane.follow(john)
|
109
|
-
|
110
|
-
snippet = '<a href="/1">John</a> says: How\'s it going ' +
|
111
|
-
'<a href="/user/2">jane</a>?'
|
112
|
-
|
113
|
-
john.broadcast(snippet)
|
114
|
-
|
115
|
-
assert jane.activities.include?(snippet)
|
116
|
-
end
|
117
|
-
|
118
|
-
#### Saving Space
|
119
|
-
|
120
|
-
# In most cases, users don't really care about keeping their entire activity
|
121
|
-
# history. This application requirement would be fairly trivial to implement.
|
122
|
-
|
123
|
-
# Let's reopen our `User` class and define a new broadcast method.
|
124
|
-
class User
|
125
|
-
# We define a constant where we set the maximum number of activity entries.
|
126
|
-
MAX = 10
|
127
|
-
|
128
|
-
# Using `MAX` as the reference, we check if the number of activities exceeds
|
129
|
-
# `MAX`, and use
|
130
|
-
# [ltrim](http://code.google.com/p/redis/wiki/LtrimCommand) to truncate
|
131
|
-
# the activities.
|
132
|
-
def broadcast(str)
|
133
|
-
followers.each do |user|
|
134
|
-
user.key[:activities].lpush(str)
|
135
|
-
|
136
|
-
if user.key[:activities].llen > MAX
|
137
|
-
user.key[:activities].ltrim(0, MAX - 1)
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
# Now let's verify that this new behavior is enforced.
|
144
|
-
test "pushing 11 activities maintains the list to 10" do |john, jane|
|
145
|
-
jane.follow(john)
|
146
|
-
|
147
|
-
11.times { john.broadcast("Flooding your feed!") }
|
148
|
-
|
149
|
-
assert 10 == jane.activities.size
|
150
|
-
end
|
151
|
-
|
152
|
-
|
153
|
-
#### Conclusion
|
154
|
-
|
155
|
-
# As you can see, choosing a more straightforward approach (in this case,
|
156
|
-
# actually having a list per user, instead of maintaining a separate
|
157
|
-
# `activities` table) will greatly simplify the design of your system.
|
158
|
-
#
|
159
|
-
# As a final note, keep in mind that the Ohm solution would still need
|
160
|
-
# sharding for large datasets, but that would be again trivial to implement
|
161
|
-
# using [redis-rb](http://github.com/ezmobius/redis-rb)'s distributed support
|
162
|
-
# and sharding it against the *user_id*.
|