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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a233394733ef9467944e0803a078e296f14f5a9b
4
- data.tar.gz: 7c9f2a402a570e18428590d81d4dcf146c2b3f7f
3
+ metadata.gz: a2d4f419606a66335e7a44d8440732b4179a068f
4
+ data.tar.gz: 6565464772af7ad624fb92268cac9ede60f70b44
5
5
  SHA512:
6
- metadata.gz: c7397fe0d14eea5851e5b7037590fcc24063a0dbe5dcdd31fccdd3dfff2fec88dd313274f4d233a5c6a73d1d25aaebb1146f8889bfa86139482f77009f08d966
7
- data.tar.gz: 407951baa343da76d535067e72ac31defa8ab0c763f5d76f283bf4bab99474b415a6894f68b61d58c0a197d7706496cfb5af099d59ea7763fd582d391c676847
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
- # User.find(:tag => ["ruby", "python"]).include?(u)
792
- # # => true
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
- Sohm::MultiSet.new(key, self, Command.new(:sinterstore, *keys))
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
- # Example:
1105
- #
1106
- # class User < Sohm::Model; end
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.9.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.9.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-04-27 00:00:00.000000000 Z
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
@@ -1,13 +0,0 @@
1
- require_relative "common"
2
-
3
- 1000.times do |i|
4
- Event.create(:name => "Redis Meetup #{i}", :location => "At my place")
5
- end
6
-
7
- benchmark "Delete event" do
8
- Event.all.each do |event|
9
- event.delete
10
- end
11
- end
12
-
13
- run 1
@@ -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*.