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 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*.