shameless 0.2.0 → 0.3.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: 3243fd35d7a9377bdaccb46cfcbacd03be44c8c7
4
- data.tar.gz: 9d18df882c3964ca57a387199f191dbfdcbbfd61
3
+ metadata.gz: ffc551d90e92b4f9174e15573bfdefb8a327c20f
4
+ data.tar.gz: 4e721f69cad576d714069eb2cdb281c13e98d256
5
5
  SHA512:
6
- metadata.gz: 7b56807b2507d10c86dbdf502b2031fab3beb79024f385e130bca212a15eb6dca6cd2dc0d7a49e2451f947a9875fb8c3b3e423ef894e7429707991469ffdd524
7
- data.tar.gz: 9c94897619fd4978e639ff806dcc88625844f13683adc660b28213a67fa64aade9701f23430312ed9b795afa255542beeae0727ce472a7d4f5f3acec0d791231
6
+ metadata.gz: 2ea84086ac343ccba0b8a44c5c257dfa02130355ffc5efd71dee633db5df9cc6631b15c8d2e24999bf1f211d663caf694664d9e2c5b561708aedd43d91769e1a
7
+ data.tar.gz: 9d939c4abd011c615cdda23894dd0e82459304b88a487d57de11006f100c3ac0b791fada345520cd6c8d2227e3352d937f69ceb4f1ab379e498d859cf8c8fa1d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ ### 0.3.0 (2016-11-18)
2
+
3
+ * Add `Cell#uuid`
4
+ * Add `Cell#id`
5
+ * Add `Model.fetch_latest_cells`
6
+ * Initialize `ref_key` with zero, not one
7
+ * Add `Model#present?` and `Cell#present?`
8
+ * Allow `Cell#save` to be called even without making any changes
9
+ * Add `Model#fetch` and `Cell#fetch`
10
+ * Add `Model#reload` and `Cell#reload`
11
+ * Add `Model#previous` and `Cell#previous`
12
+ * `Model.put` now correctly looks up and updates an existing instance
13
+ * Add `Model#update` and `Cell#update`
14
+ * Expose `Model#base`
15
+ * Add `Configuration#legacy_created_at_is_bigint`
16
+ * Keep a reference to only one model class per table name
17
+ * Make `Store#find_shard` public
18
+ * Make `Store#each_shard` public
19
+ * Name index tables `*_:name_index_*`
20
+ * Add `Configuration#database_extensions`, they're being passed to the Sequel adapter
21
+ * Add `Configuration#connection_options`, they're being passed to the Sequel adapter
22
+ * Don't prefix table names with underscore when store name is `nil`
23
+ * Add `Store#each_partition`
24
+ * Add `Store#disconnect`
25
+
1
26
  ### 0.2.0 (2016-11-14)
2
27
 
3
28
  * Add `Store#padded_shard` to get the formatted shard number for a shardable value
data/README.md CHANGED
@@ -44,6 +44,8 @@ The core object of shameless is a `Store`. Here's how you can set one up:
44
44
  RateStore = Shameless::Store.new(:rate_store) do |c|
45
45
  c.partition_urls = [ENV['RATE_STORE_DATABASE_URL_0'], ENV['RATE_STORE_DATABASE_URL_1']
46
46
  c.shards_count = 512 # total number of shards across all partitions
47
+ c.connection_options = {max_connections: 10} # connection options passed to Sequel.connect
48
+ c.database_extensions = [:newrelic_instrumentation]
47
49
  end
48
50
  ```
49
51
 
@@ -93,7 +95,7 @@ class Rate
93
95
  end
94
96
  ```
95
97
 
96
- The default index is called a primary index, the corresponding tables would be called `rate_store_rate_primary_[000000-000511]`. You can add additional indices you'd like to query by:
98
+ The default index is called a primary index, the corresponding tables would be called `rate_store_rate_primary_index_[000000-000511]`. You can add additional indices you'd like to query by:
97
99
 
98
100
  ```ruby
99
101
  class Rate
@@ -5,13 +5,20 @@ module Shameless
5
5
  BASE = 'base'
6
6
 
7
7
  def self.base(model, body)
8
- new(model, BASE, body)
8
+ serialized_body = serialize_body(body)
9
+ new(model, BASE, body: serialized_body)
9
10
  end
10
11
 
11
- def initialize(model, name, body = nil)
12
+ def self.serialize_body(body)
13
+ MessagePack.pack(body)
14
+ end
15
+
16
+ attr_reader :model, :name, :id
17
+
18
+ def initialize(model, name, values = nil)
12
19
  @model = model
13
20
  @name = name
14
- @body = body
21
+ initialize_from_values(values)
15
22
  end
16
23
 
17
24
  def [](key)
@@ -24,32 +31,65 @@ module Shameless
24
31
  end
25
32
 
26
33
  def save
34
+ load
27
35
  @created_at = Time.now
28
- @ref_key ||= 0
36
+ @created_at = (@created_at.to_f * 1000).to_i if @model.class.store.configuration.legacy_created_at_is_bigint
37
+ @ref_key ||= -1
29
38
  @ref_key += 1
30
39
  @model.put_cell(cell_values)
31
40
  end
32
41
 
42
+ def update(values)
43
+ values.each do |key, value|
44
+ self[key] = value
45
+ end
46
+
47
+ save
48
+ end
49
+
33
50
  def ref_key
34
- fetch
51
+ load
35
52
  @ref_key
36
53
  end
37
54
 
38
55
  def created_at
39
- fetch
56
+ load
40
57
  @created_at
41
58
  end
42
59
 
43
60
  def body
44
- fetch
61
+ load
45
62
  @body
46
63
  end
47
64
 
65
+ def previous
66
+ if ref_key && previous_cell_values = @model.fetch_cell(@name, ref_key - 1)
67
+ self.class.new(@model, @name, previous_cell_values)
68
+ end
69
+ end
70
+
71
+ def reload
72
+ @body = @ref_key = @created_at = nil
73
+ end
74
+
75
+ def fetch(key, default)
76
+ body.key?(key.to_s) ? self[key] : default
77
+ end
78
+
79
+ def present?
80
+ load
81
+ !@ref_key.nil?
82
+ end
83
+
84
+ def uuid
85
+ @model.uuid
86
+ end
87
+
48
88
  private
49
89
 
50
90
  def cell_values
51
91
  {
52
- uuid: @model.uuid,
92
+ uuid: uuid,
53
93
  column_name: @name,
54
94
  ref_key: ref_key,
55
95
  created_at: created_at,
@@ -58,7 +98,7 @@ module Shameless
58
98
  end
59
99
 
60
100
  def serialized_body
61
- MessagePack.pack(body)
101
+ self.class.serialize_body(body)
62
102
  end
63
103
 
64
104
  def deserialize_body(body)
@@ -67,12 +107,20 @@ module Shameless
67
107
 
68
108
  private
69
109
 
70
- def fetch
110
+ def load
71
111
  if @body.nil?
72
112
  values = @model.fetch_cell(@name)
73
- @ref_key = values[:ref_key] if values
74
- @created_at = values[:created_at] if values
75
- @body = values ? deserialize_body(values[:body]) : {}
113
+ initialize_from_values(values)
114
+ @body ||= {}
115
+ end
116
+ end
117
+
118
+ def initialize_from_values(values)
119
+ if values
120
+ @id = values[:id]
121
+ @body = deserialize_body(values[:body])
122
+ @ref_key = values[:ref_key]
123
+ @created_at = values[:created_at]
76
124
  end
77
125
  end
78
126
  end
@@ -1,6 +1,8 @@
1
1
  module Shameless
2
2
  class Configuration
3
- attr_accessor :partition_urls, :shards_count
3
+ attr_accessor :partition_urls, :shards_count, :connection_options, :database_extensions
4
+
5
+ attr_accessor :legacy_created_at_is_bigint
4
6
 
5
7
  def shards_per_partition_count
6
8
  shards_count / partitions_count
@@ -31,18 +31,33 @@ module Shameless
31
31
 
32
32
  def put(values)
33
33
  shardable_value = values.fetch(@shard_on)
34
- index_values = (@columns.keys + [:uuid]).each_with_object({}) {|column, o| o[column] = values.fetch(column) }
34
+ index_values = index_values(values, true)
35
35
 
36
36
  @model.store.put(table_name, shardable_value, index_values)
37
37
  end
38
38
 
39
39
  def where(query)
40
40
  shardable_value = query.fetch(@shard_on)
41
+ query = index_values(query, false)
41
42
  @model.store.where(table_name, shardable_value, query).map {|r| @model.new(r[:uuid]) }
42
43
  end
43
44
 
44
45
  def table_name
45
- "#{@model.table_name}_#{@name}"
46
+ "#{@model.table_name}_#{full_name}"
47
+ end
48
+
49
+ def full_name
50
+ "#{@name}_index"
51
+ end
52
+
53
+ def index_values(values, all_required)
54
+ (@columns.keys + [:uuid]).each_with_object({}) do |column, o|
55
+ if all_required
56
+ o[column] = values.fetch(column)
57
+ else
58
+ o[column] = values[column] if values.key?(column)
59
+ end
60
+ end
46
61
  end
47
62
 
48
63
  def create_tables!
@@ -57,8 +72,12 @@ module Shameless
57
72
  end
58
73
  end
59
74
 
75
+ def column?(key)
76
+ @columns.keys.any? {|c| c.to_s == key.to_s }
77
+ end
78
+
60
79
  def prevent_readonly_attribute_mutation!(key)
61
- if @columns.keys.any? {|c| c.to_s == key.to_s }
80
+ if column?(key)
62
81
  raise ReadonlyAttributeMutation, "The attribute #{key} cannot be modified because it's part of the #{@name} index"
63
82
  end
64
83
  end
@@ -17,7 +17,7 @@ module Shameless
17
17
  index = Index.new(name, self, &block)
18
18
  @indices << index
19
19
 
20
- define_singleton_method("#{index.name}_index") { index }
20
+ define_singleton_method(index.full_name) { index }
21
21
  end
22
22
 
23
23
  def cell(name)
@@ -30,13 +30,19 @@ module Shameless
30
30
  end
31
31
 
32
32
  def put(values)
33
- uuid = SecureRandom.uuid
34
-
35
- new(uuid, values).tap do |model|
36
- model.save
37
-
38
- index_values = values.merge(uuid: uuid)
39
- @indices.each {|i| i.put(index_values) }
33
+ if model = where(values).first
34
+ model_values = reject_index_values(values)
35
+ model.update(model_values)
36
+ model
37
+ else
38
+ uuid = SecureRandom.uuid
39
+
40
+ new(uuid, values).tap do |model|
41
+ model.save
42
+
43
+ index_values = values.merge(uuid: uuid)
44
+ @indices.each {|i| i.put(index_values) }
45
+ end
40
46
  end
41
47
  end
42
48
 
@@ -44,14 +50,28 @@ module Shameless
44
50
  @store.put(table_name, shardable_value, cell_values)
45
51
  end
46
52
 
47
- def fetch_cell(shardable_value, uuid, cell_name)
53
+ def fetch_cell(shardable_value, uuid, cell_name, ref_key)
48
54
  query = {uuid: uuid, column_name: cell_name}
55
+ query[:ref_key] = ref_key if ref_key
49
56
 
50
57
  @store.where(table_name, shardable_value, query).order(:ref_key).last
51
58
  end
52
59
 
60
+ def fetch_latest_cells(shard:, cursor:, limit:)
61
+ query = ['id > ?', cursor]
62
+ @store.where(table_name, shard, query).limit(limit).map do |cell_values|
63
+ model = new(cell_values[:uuid])
64
+ name = cell_values[:column_name].to_sym
65
+ Cell.new(model, name, cell_values)
66
+ end
67
+ end
68
+
53
69
  def table_name
54
- "#{@store.name}_#{@name}"
70
+ [@store.name, @name].compact.join('_')
71
+ end
72
+
73
+ def table_names
74
+ [table_name, *@indices.map(&:table_name)]
55
75
  end
56
76
 
57
77
  def create_tables!
@@ -61,7 +81,9 @@ module Shameless
61
81
  t.varchar :column_name, null: false
62
82
  t.integer :ref_key, null: false
63
83
  t.mediumblob :body
64
- t.datetime :created_at, null: false
84
+
85
+ created_at_type = @store.configuration.legacy_created_at_is_bigint ? :bigint : :datetime
86
+ t.column :created_at, created_at_type, null: false
65
87
 
66
88
  t.index %i[uuid column_name ref_key], unique: true
67
89
  end
@@ -73,6 +95,10 @@ module Shameless
73
95
  primary_index.where(query)
74
96
  end
75
97
 
98
+ def reject_index_values(values)
99
+ values.reject {|k, _| @indices.any? {|i| i.column?(k) } }
100
+ end
101
+
76
102
  def prevent_readonly_attribute_mutation!(key)
77
103
  @indices.each {|i| i.prevent_readonly_attribute_mutation!(key) }
78
104
  end
@@ -80,7 +106,7 @@ module Shameless
80
106
  private
81
107
 
82
108
  module InstanceMethods
83
- attr_reader :uuid
109
+ attr_reader :uuid, :base
84
110
 
85
111
  def initialize(uuid, base_body = nil)
86
112
  @uuid = uuid
@@ -95,6 +121,10 @@ module Shameless
95
121
  @base[field] = value
96
122
  end
97
123
 
124
+ def update(values)
125
+ @base.update(values)
126
+ end
127
+
98
128
  def save
99
129
  @base.save
100
130
  end
@@ -107,12 +137,28 @@ module Shameless
107
137
  @base.created_at
108
138
  end
109
139
 
140
+ def previous
141
+ @base.previous
142
+ end
143
+
144
+ def reload
145
+ @base.reload
146
+ end
147
+
148
+ def fetch(key, default)
149
+ @base.fetch(key, default)
150
+ end
151
+
152
+ def present?
153
+ @base.present?
154
+ end
155
+
110
156
  def put_cell(cell_values)
111
157
  self.class.put_cell(shardable_value, cell_values)
112
158
  end
113
159
 
114
- def fetch_cell(cell_name)
115
- self.class.fetch_cell(shardable_value, uuid, cell_name)
160
+ def fetch_cell(cell_name, ref_key = nil)
161
+ self.class.fetch_cell(shardable_value, uuid, cell_name, ref_key)
116
162
  end
117
163
 
118
164
  def prevent_readonly_attribute_mutation!(key)
@@ -4,7 +4,7 @@ require 'shameless/model'
4
4
 
5
5
  module Shameless
6
6
  class Store
7
- attr_reader :name
7
+ attr_reader :name, :configuration
8
8
 
9
9
  def initialize(name, &block)
10
10
  @name = name
@@ -15,8 +15,7 @@ module Shameless
15
15
  def attach(model_class, name = nil)
16
16
  model_class.extend(Model)
17
17
  model_class.attach_to(self, name)
18
- @models ||= []
19
- @models << model_class
18
+ models_hash[name] = model_class
20
19
  end
21
20
 
22
21
  def put(table_name, shardable_value, values)
@@ -27,8 +26,20 @@ module Shameless
27
26
  find_table(table_name, shardable_value).where(query)
28
27
  end
29
28
 
29
+ def disconnect
30
+ if instance_variable_defined?(:@partitions)
31
+ partitions.each(&:disconnect)
32
+ end
33
+ end
34
+
35
+ def each_partition(&block)
36
+ partitions.each do |partition|
37
+ block.call(partition, table_names_on_partition(partition))
38
+ end
39
+ end
40
+
30
41
  def create_tables!
31
- @models.each(&:create_tables!)
42
+ models.each(&:create_tables!)
32
43
  end
33
44
 
34
45
  def create_table!(table_name, &block)
@@ -44,14 +55,42 @@ module Shameless
44
55
  format_shard(shard)
45
56
  end
46
57
 
58
+ def each_shard(&block)
59
+ 0.upto(@configuration.shards_count - 1, &block)
60
+ end
61
+
62
+ def find_shard(shardable_value)
63
+ shardable_value % @configuration.shards_count
64
+ end
65
+
47
66
  private
48
67
 
68
+ def models_hash
69
+ @models_hash ||= {}
70
+ end
71
+
72
+ def models
73
+ models_hash.values
74
+ end
75
+
49
76
  def partitions
50
- @partitions ||= @configuration.partition_urls.map {|url| Sequel.connect(url) }
77
+ @partitions ||= @configuration.partition_urls.map {|url| connect(url) }
51
78
  end
52
79
 
53
- def each_shard(&block)
54
- 0.upto(@configuration.shards_count - 1, &block)
80
+ def connect(url)
81
+ Sequel.connect(url, @configuration.connection_options || Sequel::OPTS).tap do |db|
82
+ db.extension *@configuration.database_extensions
83
+ end
84
+ end
85
+
86
+ def table_names_on_partition(partition)
87
+ partition_index = partitions.index(partition)
88
+ first_shard = partition_index * @configuration.shards_per_partition_count
89
+ last_shard = first_shard + @configuration.shards_per_partition_count - 1
90
+ shards = first_shard..last_shard
91
+ table_names = models.flat_map(&:table_names)
92
+
93
+ table_names.flat_map {|t| shards.map {|s| table_name_with_shard(t, s) } }
55
94
  end
56
95
 
57
96
  def table_name_with_shard(table_name, shard)
@@ -63,10 +102,6 @@ module Shameless
63
102
  shard.to_s.rjust(6, '0')
64
103
  end
65
104
 
66
- def find_shard(shardable_value)
67
- shardable_value % @configuration.shards_count
68
- end
69
-
70
105
  def find_table(table_name, shardable_value)
71
106
  shard = find_shard(shardable_value)
72
107
  partition = find_partition_for_shard(shard)
@@ -1,3 +1,3 @@
1
1
  module Shameless
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shameless
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Olek Janiszewski
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-11-14 00:00:00.000000000 Z
11
+ date: 2016-11-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack