shameless 0.2.0 → 0.3.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: 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