sohm 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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*.
|