shameless 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +204 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/shameless.rb +2 -0
- data/lib/shameless/cell.rb +79 -0
- data/lib/shameless/configuration.rb +13 -0
- data/lib/shameless/errors.rb +4 -0
- data/lib/shameless/index.rb +66 -0
- data/lib/shameless/model.rb +129 -0
- data/lib/shameless/store.rb +69 -0
- data/lib/shameless/version.rb +3 -0
- data/shameless.gemspec +27 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f6279a53b06a5c2ceeb839ecc50eca06951c12f0
|
4
|
+
data.tar.gz: d8dd4210688a3f72e1ea7574ceeb9f2cb690796d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3f69323c0db8d7fbf99018da32f743c6155f98766bd021d92edd9d1932d30294a0c145ca1be5514f2d6a9883ebde836f0e58d5f9fb3aca8fa278447d0e971621
|
7
|
+
data.tar.gz: d0843089e84f7cc1125166338d1fa38bd8dca4cd5cc3b4fede32cdf79b11854bc86a287e811c9111416b8f9c8111b74cfcfae70c284a6659ae4756b82525901a
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 HotelTonight
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
# Shameless
|
2
|
+
|
3
|
+
Shameless is an implementation of a schemaless, distributed, append-only store built on top of MySQL and the Sequel gem. It was extracted from a battle-tested codebase of our main application at HotelTonight. Since it's using Sequel for database access, it could work on any database, e.g. postgres, although we've only used it with MySQL.
|
4
|
+
|
5
|
+
## Background
|
6
|
+
|
7
|
+
Shameless was born out of our need to have highly scalable, distributed storage for hotel rates. Rates are a way hotels package their rooms, they typically include check-in and check-out date, room type, rate plan, net price, discount, extra services, etc. Our original solution of storing rates in a typical relational SQL table was reaching its limits due to write congestion, migration anxiety, and high maintenance.
|
8
|
+
|
9
|
+
Hotel rates change very frequently, so our solution needed to have consistent write latency. There are also mutliple agents mutating various aspects of those rates, so we wanted something that would enable versioning. We also wanted to avoid having to create migrations whenever we were adding more data to rates.
|
10
|
+
|
11
|
+
## Concept
|
12
|
+
|
13
|
+
The whole idea of Shameless is to split a regular SQL table into index tables and content tables. Index tables map the fields you want to query by to UUIDs, content tables map UUIDs to model contents (bodies). In addition, both index and content tables are sharded.
|
14
|
+
|
15
|
+
The body of the model is schema-less, you can store an arbitrary data structures. Under the hood, the body is serialized using MessagePack and stored as a blob in a single database column (hence the need for index tables).
|
16
|
+
|
17
|
+
The process of querying for records can be described as:
|
18
|
+
|
19
|
+
1. Query the index tables by index fields (e.g. hotel ID, check-in date, and length of stay), sharded by hotel ID, getting get back a list of UUIDs
|
20
|
+
2. Query the content tables, sharded by UUID, for most recent version of model
|
21
|
+
|
22
|
+
Inserting a record is similar:
|
23
|
+
|
24
|
+
1. Generate a UUID
|
25
|
+
2. Serialize and write model content into appropriate shard of the content tables
|
26
|
+
2. Insert a row (index fields + model UUID) to the appropriate shard of the index table
|
27
|
+
|
28
|
+
Inserting a new version of an existing record is even simpler:
|
29
|
+
|
30
|
+
1. Increment version
|
31
|
+
2. Serialize and write model content into appropriate shard of the content tables
|
32
|
+
|
33
|
+
Naturally, shameless hides all that complexity behind a straight-forward API.
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
### Creating a store
|
38
|
+
|
39
|
+
The core object of shameless is a `Store`. Here's how you can set one up:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# config/initializers/rate_store.rb
|
43
|
+
|
44
|
+
RateStore = Shameless::Store.new(:rate_store) do |c|
|
45
|
+
c.partition_urls = [ENV['RATE_STORE_DATABASE_URL_0'], ENV['RATE_STORE_DATABASE_URL_1']
|
46
|
+
c.shards_count = 512 # total number of shards across all partitions
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
The initializer argument (`:rate_store`) defines the namespace by which all tables will be prefixed, in this case `rate_store_`.
|
51
|
+
|
52
|
+
Once you've got the Store configured, you can declare models.
|
53
|
+
|
54
|
+
### Declaring models
|
55
|
+
|
56
|
+
Models specify the kinds of entities you want to persist in your store. Models are simple Ruby classes (even anonymous) that you attach to a `Store` using `Store#attach_to(model)`, e.g.:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# app/models/rate.rb
|
60
|
+
|
61
|
+
class Rate
|
62
|
+
RateStore.attach(self)
|
63
|
+
|
64
|
+
# ...
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
By default, this will map to tables called `rate_store_rate_[000000-000511]` by lowercasing the class name. You can also provide the table namespace using a second argument, e.g.:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
my_model = Class.new do
|
72
|
+
RateStore.attach(self, :rates)
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
A model is useless without indices. Let's see how to define them.
|
77
|
+
|
78
|
+
### Defining indices
|
79
|
+
|
80
|
+
Indices are a crucial component of shameless. They allow us to perform fast lookups for model UUIDs. Here's how you define an index:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
class Rate
|
84
|
+
RateStore.attach(self)
|
85
|
+
|
86
|
+
index do
|
87
|
+
integer :hotel_id
|
88
|
+
string :room_type
|
89
|
+
string :check_in_date # at the moment, only integer and string types are supported
|
90
|
+
|
91
|
+
shard_on :hotel_id # required, values need to be numeric
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
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:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
class Rate
|
100
|
+
RateStore.attach(self)
|
101
|
+
|
102
|
+
index do
|
103
|
+
# ..
|
104
|
+
end
|
105
|
+
|
106
|
+
index :secondary do
|
107
|
+
integer :hotel_id
|
108
|
+
string :gateway
|
109
|
+
string :discount_type
|
110
|
+
|
111
|
+
shard_on :hotel_id
|
112
|
+
end
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
### Defining cells
|
117
|
+
|
118
|
+
Model content is stored in blobs called "cells". You can think of cells as separate model columns that can store rich data structures and can change independently over time. The default cell is called "base" (that's what all model-level accessors delegate to), but you can declare additional cells using `Model.cell`:
|
119
|
+
|
120
|
+
```
|
121
|
+
class Rate
|
122
|
+
RateStore.attach(self)
|
123
|
+
|
124
|
+
index do
|
125
|
+
# ..
|
126
|
+
end
|
127
|
+
|
128
|
+
cell :meta
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
### Reading/writing
|
133
|
+
|
134
|
+
To insert and query the model, use `Model.put` and `Model.where`:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
# Writing - all index fields are required, the rest is the schemaless content
|
138
|
+
rate = Rate.put(hotel_id: 1, room_type: '1 bed', check_in_date: Date.today, gateway: 'pegasus', discount_type: 'geo', net_price: 120.0)
|
139
|
+
rate[:net_price] # => 120.0 # access in the "base" cell
|
140
|
+
|
141
|
+
# Create a new version of the "base" cell
|
142
|
+
rate[:net_price] = 130.0
|
143
|
+
rate.save
|
144
|
+
|
145
|
+
# Reading from/writing to a different cell is simple, too:
|
146
|
+
rate.meta[:hotel_enabled] = true
|
147
|
+
rate.meta.save
|
148
|
+
|
149
|
+
# Querying by primary index
|
150
|
+
rates = Rate.where(hotel_id: 1, room_type: '1 bed', check_in_date: Date.today)
|
151
|
+
|
152
|
+
# Querying by a named index
|
153
|
+
rates = Rate.secondary.where(hotel_id: 1, gateway: 'pegasus', discount_type: 'geo')
|
154
|
+
rates.first[:net_price] # => 130.0
|
155
|
+
```
|
156
|
+
|
157
|
+
### Creating tables
|
158
|
+
|
159
|
+
To create all shards for all tables, across all partitions, run:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
RateStore.create_tables!
|
163
|
+
```
|
164
|
+
|
165
|
+
This will create the underlying index tables, content tables, together with database indices for fast access.
|
166
|
+
|
167
|
+
## Installation
|
168
|
+
|
169
|
+
Add this line to your application's Gemfile:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
gem 'shameless'
|
173
|
+
```
|
174
|
+
|
175
|
+
And then execute:
|
176
|
+
|
177
|
+
$ bundle
|
178
|
+
|
179
|
+
Or install it yourself as:
|
180
|
+
|
181
|
+
$ gem install shameless
|
182
|
+
|
183
|
+
## Development
|
184
|
+
|
185
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
186
|
+
|
187
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
188
|
+
|
189
|
+
## Contributing
|
190
|
+
|
191
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/hoteltonight/shameless.
|
192
|
+
|
193
|
+
|
194
|
+
## License
|
195
|
+
|
196
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
197
|
+
|
198
|
+
## Credits
|
199
|
+
|
200
|
+
Shameless was inspired by the following resources:
|
201
|
+
|
202
|
+
- Uber: https://eng.uber.com/schemaless-part-one
|
203
|
+
- FriendFeed: https://backchannel.org/blog/friendfeed-schemaless-mysql
|
204
|
+
- Pinterest: https://engineering.pinterest.com/blog/sharding-pinterest-how-we-scaled-our-mysql-fleet
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "shameless"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/shameless.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
|
3
|
+
module Shameless
|
4
|
+
class Cell
|
5
|
+
BASE = 'base'
|
6
|
+
|
7
|
+
def self.base(model, body)
|
8
|
+
new(model, BASE, body)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(model, name, body = nil)
|
12
|
+
@model = model
|
13
|
+
@name = name
|
14
|
+
@body = body
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](key)
|
18
|
+
body[key.to_s]
|
19
|
+
end
|
20
|
+
|
21
|
+
def []=(key, value)
|
22
|
+
@model.prevent_readonly_attribute_mutation!(key)
|
23
|
+
body[key.to_s] = value
|
24
|
+
end
|
25
|
+
|
26
|
+
def save
|
27
|
+
@created_at = Time.now
|
28
|
+
@ref_key ||= 0
|
29
|
+
@ref_key += 1
|
30
|
+
@model.put_cell(cell_values)
|
31
|
+
end
|
32
|
+
|
33
|
+
def ref_key
|
34
|
+
fetch
|
35
|
+
@ref_key
|
36
|
+
end
|
37
|
+
|
38
|
+
def created_at
|
39
|
+
fetch
|
40
|
+
@created_at
|
41
|
+
end
|
42
|
+
|
43
|
+
def body
|
44
|
+
fetch
|
45
|
+
@body
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def cell_values
|
51
|
+
{
|
52
|
+
uuid: @model.uuid,
|
53
|
+
column_name: @name,
|
54
|
+
ref_key: ref_key,
|
55
|
+
created_at: created_at,
|
56
|
+
body: serialized_body
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def serialized_body
|
61
|
+
MessagePack.pack(body)
|
62
|
+
end
|
63
|
+
|
64
|
+
def deserialize_body(body)
|
65
|
+
MessagePack.unpack(body)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def fetch
|
71
|
+
if @body.nil?
|
72
|
+
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]) : {}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'shameless/errors'
|
2
|
+
|
3
|
+
module Shameless
|
4
|
+
class Index
|
5
|
+
PRIMARY = :primary
|
6
|
+
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
def initialize(name, model, &block)
|
10
|
+
@name = name || PRIMARY
|
11
|
+
@model = model
|
12
|
+
instance_eval(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
DataTypes = %i[integer string]
|
16
|
+
|
17
|
+
DataTypes.each do |type|
|
18
|
+
define_method(type) do |column|
|
19
|
+
self.column(column, type)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def column(name, type)
|
24
|
+
@columns ||= {}
|
25
|
+
@columns[name] = type
|
26
|
+
end
|
27
|
+
|
28
|
+
def shard_on(shard_on)
|
29
|
+
@shard_on = shard_on
|
30
|
+
end
|
31
|
+
|
32
|
+
def put(values)
|
33
|
+
shardable_value = values.fetch(@shard_on)
|
34
|
+
index_values = (@columns.keys + [:uuid]).each_with_object({}) {|column, o| o[column] = values.fetch(column) }
|
35
|
+
|
36
|
+
@model.store.put(table_name, shardable_value, index_values)
|
37
|
+
end
|
38
|
+
|
39
|
+
def where(query)
|
40
|
+
shardable_value = query.fetch(@shard_on)
|
41
|
+
@model.store.where(table_name, shardable_value, query).map {|r| @model.new(r[:uuid]) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def table_name
|
45
|
+
"#{@model.table_name}_#{@name}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_tables!
|
49
|
+
@model.store.create_table!(table_name) do |t|
|
50
|
+
@columns.each do |name, type|
|
51
|
+
t.column name, type, null: false
|
52
|
+
end
|
53
|
+
|
54
|
+
t.varchar :uuid, size: 36
|
55
|
+
|
56
|
+
t.index @columns.keys, unique: true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def prevent_readonly_attribute_mutation!(key)
|
61
|
+
if @columns.keys.any? {|c| c.to_s == key.to_s }
|
62
|
+
raise ReadonlyAttributeMutation, "The attribute #{key} cannot be modified because it's part of the #{@name} index"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'shameless/index'
|
2
|
+
require 'shameless/cell'
|
3
|
+
|
4
|
+
module Shameless
|
5
|
+
module Model
|
6
|
+
attr_reader :store
|
7
|
+
|
8
|
+
def attach_to(store, name)
|
9
|
+
@store = store
|
10
|
+
@name = name || self.name.downcase # TODO use activesupport?
|
11
|
+
|
12
|
+
include(InstanceMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
def index(name = nil, &block)
|
16
|
+
@indices ||= []
|
17
|
+
index = Index.new(name, self, &block)
|
18
|
+
@indices << index
|
19
|
+
|
20
|
+
define_singleton_method("#{index.name}_index") { index }
|
21
|
+
end
|
22
|
+
|
23
|
+
def cell(name)
|
24
|
+
name = name.to_s
|
25
|
+
|
26
|
+
define_method(name) do
|
27
|
+
@cells ||= {}
|
28
|
+
@cells[name] ||= Cell.new(self, name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
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) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def put_cell(shardable_value, cell_values)
|
44
|
+
@store.put(table_name, shardable_value, cell_values)
|
45
|
+
end
|
46
|
+
|
47
|
+
def fetch_cell(shardable_value, uuid, cell_name)
|
48
|
+
query = {uuid: uuid, column_name: cell_name}
|
49
|
+
|
50
|
+
@store.where(table_name, shardable_value, query).order(:ref_key).last
|
51
|
+
end
|
52
|
+
|
53
|
+
def table_name
|
54
|
+
"#{@store.name}_#{@name}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_tables!
|
58
|
+
@store.create_table!(table_name) do |t|
|
59
|
+
t.primary_key :id
|
60
|
+
t.varchar :uuid, size: 36
|
61
|
+
t.varchar :column_name, null: false
|
62
|
+
t.integer :ref_key, null: false
|
63
|
+
t.mediumblob :body
|
64
|
+
t.datetime :created_at, null: false
|
65
|
+
|
66
|
+
t.index %i[uuid column_name ref_key], unique: true
|
67
|
+
end
|
68
|
+
|
69
|
+
@indices.each(&:create_tables!)
|
70
|
+
end
|
71
|
+
|
72
|
+
def where(query)
|
73
|
+
primary_index.where(query)
|
74
|
+
end
|
75
|
+
|
76
|
+
def prevent_readonly_attribute_mutation!(key)
|
77
|
+
@indices.each {|i| i.prevent_readonly_attribute_mutation!(key) }
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
module InstanceMethods
|
83
|
+
attr_reader :uuid
|
84
|
+
|
85
|
+
def initialize(uuid, base_body = nil)
|
86
|
+
@uuid = uuid
|
87
|
+
@base = Cell.base(self, base_body)
|
88
|
+
end
|
89
|
+
|
90
|
+
def [](field)
|
91
|
+
@base[field]
|
92
|
+
end
|
93
|
+
|
94
|
+
def []=(field, value)
|
95
|
+
@base[field] = value
|
96
|
+
end
|
97
|
+
|
98
|
+
def save
|
99
|
+
@base.save
|
100
|
+
end
|
101
|
+
|
102
|
+
def ref_key
|
103
|
+
@base.ref_key
|
104
|
+
end
|
105
|
+
|
106
|
+
def created_at
|
107
|
+
@base.created_at
|
108
|
+
end
|
109
|
+
|
110
|
+
def put_cell(cell_values)
|
111
|
+
self.class.put_cell(shardable_value, cell_values)
|
112
|
+
end
|
113
|
+
|
114
|
+
def fetch_cell(cell_name)
|
115
|
+
self.class.fetch_cell(shardable_value, uuid, cell_name)
|
116
|
+
end
|
117
|
+
|
118
|
+
def prevent_readonly_attribute_mutation!(key)
|
119
|
+
self.class.prevent_readonly_attribute_mutation!(key)
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def shardable_value
|
125
|
+
uuid[0, 4].to_i(16)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'shameless/configuration'
|
3
|
+
require 'shameless/model'
|
4
|
+
|
5
|
+
module Shameless
|
6
|
+
class Store
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
def initialize(name, &block)
|
10
|
+
@name = name
|
11
|
+
@configuration = Configuration.new
|
12
|
+
block.call(@configuration)
|
13
|
+
end
|
14
|
+
|
15
|
+
def attach(model_class, name = nil)
|
16
|
+
model_class.extend(Model)
|
17
|
+
model_class.attach_to(self, name)
|
18
|
+
@models ||= []
|
19
|
+
@models << model_class
|
20
|
+
end
|
21
|
+
|
22
|
+
def put(table_name, shardable_value, values)
|
23
|
+
find_table(table_name, shardable_value).insert(values)
|
24
|
+
end
|
25
|
+
|
26
|
+
def where(table_name, shardable_value, query)
|
27
|
+
find_table(table_name, shardable_value).where(query)
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_tables!
|
31
|
+
@models.each(&:create_tables!)
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_table!(table_name, &block)
|
35
|
+
each_shard do |shard|
|
36
|
+
partition = find_partition_for_shard(shard)
|
37
|
+
sharded_table_name = table_name_with_shard(table_name, shard)
|
38
|
+
partition.create_table(sharded_table_name) { block.call(self) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def partitions
|
45
|
+
@partitions ||= @configuration.partition_urls.map {|url| Sequel.connect(url) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def each_shard(&block)
|
49
|
+
0.upto(@configuration.shards_count - 1, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
def table_name_with_shard(table_name, shard)
|
53
|
+
padded_shard = shard.to_s.rjust(6, '0')
|
54
|
+
"#{table_name}_#{padded_shard}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_table(table_name, shardable_value)
|
58
|
+
shard = shardable_value % @configuration.shards_count
|
59
|
+
partition = find_partition_for_shard(shard)
|
60
|
+
table_name = table_name_with_shard(table_name, shard)
|
61
|
+
partition.from(table_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_partition_for_shard(shard)
|
65
|
+
partition_index = shard / @configuration.shards_per_partition_count
|
66
|
+
partitions[partition_index]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/shameless.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'shameless/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "shameless"
|
7
|
+
spec.version = Shameless::VERSION
|
8
|
+
spec.authors = ["Olek Janiszewski"]
|
9
|
+
spec.email = ["olek@hoteltonight.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{Scalable distributed append-only data store built}
|
12
|
+
spec.homepage = "https://github.com/hoteltonight/shameless"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "msgpack"
|
21
|
+
spec.add_dependency "sequel", "~> 4.0"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
spec.add_development_dependency "sqlite3"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shameless
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Olek Janiszewski
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: msgpack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sequel
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description:
|
98
|
+
email:
|
99
|
+
- olek@hoteltonight.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".travis.yml"
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- bin/console
|
112
|
+
- bin/setup
|
113
|
+
- lib/shameless.rb
|
114
|
+
- lib/shameless/cell.rb
|
115
|
+
- lib/shameless/configuration.rb
|
116
|
+
- lib/shameless/errors.rb
|
117
|
+
- lib/shameless/index.rb
|
118
|
+
- lib/shameless/model.rb
|
119
|
+
- lib/shameless/store.rb
|
120
|
+
- lib/shameless/version.rb
|
121
|
+
- shameless.gemspec
|
122
|
+
homepage: https://github.com/hoteltonight/shameless
|
123
|
+
licenses:
|
124
|
+
- MIT
|
125
|
+
metadata: {}
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
requirements: []
|
141
|
+
rubyforge_project:
|
142
|
+
rubygems_version: 2.5.1
|
143
|
+
signing_key:
|
144
|
+
specification_version: 4
|
145
|
+
summary: Scalable distributed append-only data store built
|
146
|
+
test_files: []
|