moe 0.0.1
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 +7 -0
- data/.gitignore +19 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.markdown +66 -0
- data/Rakefile +69 -0
- data/lib/moe.rb +29 -0
- data/lib/moe/config.rb +21 -0
- data/lib/moe/dyna.rb +107 -0
- data/lib/moe/sequence.rb +23 -0
- data/lib/moe/sequence/collection.rb +56 -0
- data/lib/moe/sequence/collector.rb +58 -0
- data/lib/moe/sequence/item_fetcher.rb +44 -0
- data/lib/moe/sequence/locksmith.rb +25 -0
- data/lib/moe/sequence/metadata_item.rb +21 -0
- data/lib/moe/table_manager.rb +104 -0
- data/lib/moe/version.rb +3 -0
- data/moe.gemspec +31 -0
- data/spec/lib/config_spec.rb +26 -0
- data/spec/lib/dyna_spec.rb +113 -0
- data/spec/lib/moe_spec.rb +19 -0
- data/spec/lib/sequence/collection_spec.rb +21 -0
- data/spec/lib/sequence/collector_spec.rb +72 -0
- data/spec/lib/sequence/locksmith_spec.rb +19 -0
- data/spec/lib/sequence/metadata_item_spec.rb +24 -0
- data/spec/lib/sequence_spec.rb +27 -0
- data/spec/lib/table_manager_spec.rb +127 -0
- data/spec/spec_helper.rb +28 -0
- metadata +210 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
module Moe
|
2
|
+
module Sequence
|
3
|
+
class Collector
|
4
|
+
attr_accessor :dyna, :flushed_count, :payloads
|
5
|
+
attr_reader :owner_id, :write_tables, :uuid
|
6
|
+
|
7
|
+
def initialize(name, owner_id)
|
8
|
+
@dyna = Dyna.new
|
9
|
+
@flushed_count = 0
|
10
|
+
@payloads = []
|
11
|
+
@owner_id = owner_id
|
12
|
+
@uuid = SecureRandom.uuid
|
13
|
+
@write_tables = Moe.config.tables[name].last
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(payload={})
|
17
|
+
payloads << payload
|
18
|
+
|
19
|
+
if payloads.size >= Moe.config.batch_limit
|
20
|
+
items = keyify payloads
|
21
|
+
flush items
|
22
|
+
|
23
|
+
self.payloads = []
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def save(payload={})
|
28
|
+
metadata_item = {
|
29
|
+
"count" => (payloads.size + flushed_count).to_s,
|
30
|
+
"saved_at" => Time.now.to_s,
|
31
|
+
"payload" => MultiJson.dump(payload)
|
32
|
+
}.merge Locksmith.itemize owner_id, payload, 0, uuid
|
33
|
+
|
34
|
+
items = keyify payloads
|
35
|
+
|
36
|
+
items << metadata_item
|
37
|
+
|
38
|
+
flush items
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def flush(items)
|
44
|
+
result = dyna.batch_write_item write_tables, items
|
45
|
+
|
46
|
+
self.flushed_count += items.size
|
47
|
+
end
|
48
|
+
|
49
|
+
def keyify(items, uid=uuid)
|
50
|
+
count = flushed_count
|
51
|
+
items.each do |item|
|
52
|
+
count += 1
|
53
|
+
item.update Locksmith.key owner_id, count, uid
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Moe
|
2
|
+
module Sequence
|
3
|
+
class ItemFetcher
|
4
|
+
attr_accessor :dyna, :items, :sequence_id
|
5
|
+
attr_reader :owner_id, :table_name, :uid
|
6
|
+
|
7
|
+
def initialize(table_name, owner_id, uid)
|
8
|
+
@dyna = Dyna.new
|
9
|
+
@items = []
|
10
|
+
@owner_id = owner_id
|
11
|
+
@sequence_id = 1
|
12
|
+
@table_name = table_name
|
13
|
+
@uid = uid
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch(limit)
|
17
|
+
request = {
|
18
|
+
request_items: {
|
19
|
+
table_name => { keys: [] }
|
20
|
+
}
|
21
|
+
}
|
22
|
+
keys = request[:request_items][table_name][:keys]
|
23
|
+
|
24
|
+
limit.times do
|
25
|
+
keys << dyna.explode( Locksmith.key(owner_id, sequence_id, uid) )
|
26
|
+
|
27
|
+
self.sequence_id += 1
|
28
|
+
end
|
29
|
+
|
30
|
+
munge dyna.dynamodb.batch_get_item(request)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def munge(results)
|
36
|
+
results.responses.each_value do |item|
|
37
|
+
item.each do |i|
|
38
|
+
items << dyna.implode(i)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Moe
|
2
|
+
module Sequence
|
3
|
+
module Locksmith
|
4
|
+
|
5
|
+
module ModuleFunctions
|
6
|
+
def itemize(owner_id, payload, sequence_id, uid)
|
7
|
+
{
|
8
|
+
"hash" => owner_id,
|
9
|
+
"range" => "#{sequence_id}.#{uid}",
|
10
|
+
"payload" => MultiJson.dump(payload)
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def key(owner_id, sequence_id, uid)
|
15
|
+
{
|
16
|
+
"hash" => owner_id,
|
17
|
+
"range" => "#{sequence_id}.#{uid}"
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
extend ModuleFunctions
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Moe
|
2
|
+
module Sequence
|
3
|
+
MetadataItem = Struct.new(:table_name, :owner_id, :uid, :count, :payload) do
|
4
|
+
|
5
|
+
def items
|
6
|
+
fetcher = ItemFetcher.new table_name, owner_id, uid
|
7
|
+
remaining = count
|
8
|
+
|
9
|
+
while remaining > Moe.config.batch_limit
|
10
|
+
fetcher.fetch Moe.config.batch_limit
|
11
|
+
|
12
|
+
remaining -= Moe.config.batch_limit
|
13
|
+
end
|
14
|
+
|
15
|
+
fetcher.fetch remaining
|
16
|
+
|
17
|
+
fetcher.items
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Moe
|
2
|
+
class TableManager
|
3
|
+
attr_reader :date
|
4
|
+
attr_accessor :dyna, :meta
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@date = Time.now.strftime("%F")
|
8
|
+
@dyna = Dyna.new
|
9
|
+
@meta = dyna.find(meta_table_names.first) || dyna.create_table(meta_table_name, 2)
|
10
|
+
end
|
11
|
+
|
12
|
+
def build(model, copies=1, hash_key="hash", range_key=nil, read_capacity=5, write_capacity=10, read_tables=[])
|
13
|
+
write_tables = dyna.create_table table_name(model),
|
14
|
+
copies,
|
15
|
+
hash_key,
|
16
|
+
range_key,
|
17
|
+
read_capacity,
|
18
|
+
write_capacity
|
19
|
+
|
20
|
+
metadata = {
|
21
|
+
read_tables: read_tables << write_tables.first,
|
22
|
+
write_tables: write_tables
|
23
|
+
}
|
24
|
+
|
25
|
+
update_metadata model, metadata
|
26
|
+
|
27
|
+
[ read_tables, write_tables ]
|
28
|
+
end
|
29
|
+
|
30
|
+
def increment(model)
|
31
|
+
metadata = load_metadata model
|
32
|
+
table = load_table metadata[:write_tables].first
|
33
|
+
|
34
|
+
if table[:table_name].include? date
|
35
|
+
raise "Moe sez: Cannot increment twice on the same day!"
|
36
|
+
end
|
37
|
+
|
38
|
+
build model,
|
39
|
+
metadata[:write_tables].size,
|
40
|
+
table[:key][:hash],
|
41
|
+
table[:key][:range],
|
42
|
+
table[:read_capacity],
|
43
|
+
table[:write_capacity],
|
44
|
+
metadata[:read_tables]
|
45
|
+
end
|
46
|
+
|
47
|
+
def load_metadata(model)
|
48
|
+
metadata = dyna.get_item meta_table_names,
|
49
|
+
{ "hash" => { s: munged_model(model) } }
|
50
|
+
|
51
|
+
MultiJson.load metadata["payload"]["s"], symbolize_keys: true
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_table(table_name)
|
55
|
+
table = dyna.find table_name
|
56
|
+
|
57
|
+
{
|
58
|
+
table_name: table.table.table_name,
|
59
|
+
key: get_key(table.table.key_schema),
|
60
|
+
read_capacity: table.table.provisioned_throughput.read_capacity_units,
|
61
|
+
write_capacity: table.table.provisioned_throughput.write_capacity_units
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def meta_table_name
|
66
|
+
"moe_#{ENV['RAILS_ENV']}_manager"
|
67
|
+
end
|
68
|
+
|
69
|
+
def meta_table_names
|
70
|
+
["#{meta_table_name}_1", "#{meta_table_name}_2"]
|
71
|
+
end
|
72
|
+
|
73
|
+
def table_name(model)
|
74
|
+
"moe_#{ENV['RAILS_ENV']}_#{date}_#{munged_model(model)}".downcase
|
75
|
+
end
|
76
|
+
|
77
|
+
def update_metadata(model, payload)
|
78
|
+
item = {
|
79
|
+
"hash" => munged_model(model),
|
80
|
+
"payload" => MultiJson.dump(payload)
|
81
|
+
}
|
82
|
+
|
83
|
+
dyna.put_item meta_table_names, item
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def get_key(key_schema)
|
89
|
+
{}.tap do |key|
|
90
|
+
key_schema.each do |k|
|
91
|
+
if k.key_type == "HASH"
|
92
|
+
key[:hash] = k.attribute_name
|
93
|
+
else
|
94
|
+
key[:range] = k.attribute_name
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def munged_model(model)
|
101
|
+
model.gsub(/::/, "_")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/moe/version.rb
ADDED
data/moe.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "moe/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "moe"
|
8
|
+
spec.version = Moe::VERSION
|
9
|
+
spec.authors = ["Fuzz Leonard"]
|
10
|
+
spec.email = ["fuzz@fuzzleonard.com"]
|
11
|
+
spec.description = %q{A toolkit for working with DynamoDB at scale}
|
12
|
+
spec.summary = %q{A toolkit for working with DynamoDB}
|
13
|
+
spec.homepage = "https://github.com/Geezeo/moe"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "aws-sdk-core"
|
22
|
+
spec.add_dependency "multi_json", "~> 1.8"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "coveralls", "~> 0.7"
|
26
|
+
spec.add_development_dependency "fake_dynamo", "0.2.5"
|
27
|
+
spec.add_development_dependency "pry", "~> 0.9"
|
28
|
+
spec.add_development_dependency "rake", "~> 10"
|
29
|
+
spec.add_development_dependency "rspec", "~> 2"
|
30
|
+
spec.add_development_dependency "timecop", "~> 0.7"
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Moe::Config do
|
4
|
+
|
5
|
+
describe "config block" do
|
6
|
+
it "accepts a configuration block" do
|
7
|
+
Moe.configure do |c|
|
8
|
+
c.tables = { "foo" => "bar" }
|
9
|
+
end
|
10
|
+
|
11
|
+
expect( Moe.config.tables["foo"] ).to eq("bar")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "allows configuration changes" do
|
15
|
+
Moe.configure do |c|
|
16
|
+
c.tables = { "foo" => "bar" }
|
17
|
+
end
|
18
|
+
|
19
|
+
Moe.configure do |c|
|
20
|
+
c.tables = { "foo" => "star" }
|
21
|
+
end
|
22
|
+
|
23
|
+
expect( Moe.config.tables["foo"] ).to eq("star")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
$count = 0
|
4
|
+
|
5
|
+
describe Moe::Dyna do
|
6
|
+
let(:count) { $count += 1 }
|
7
|
+
let(:dyna) { Moe::Dyna.new }
|
8
|
+
let(:dynamodb) { Aws.dynamodb }
|
9
|
+
let(:item) { { "hash" => "test#{count}" } }
|
10
|
+
let(:dynamo_item) { dyna.explode(item) }
|
11
|
+
let(:created_tables) { dyna.create_table("Testy#{count}") }
|
12
|
+
let(:table) { dyna.find(created_tables.first).table }
|
13
|
+
|
14
|
+
describe "#batch_write_item" do
|
15
|
+
it "writes a batch of items" do
|
16
|
+
|
17
|
+
items = dyna.batch_write_item [table.table_name], [item, { "hash" => "zoo" }]
|
18
|
+
result = dyna.get_item [table.table_name], { "hash" => { s: "zoo" } }
|
19
|
+
|
20
|
+
expect( result["hash"]["s"] ).to eq("zoo")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#create_table" do
|
25
|
+
it "creates a new table" do
|
26
|
+
new_table = dyna.create_table "Testy#{count}"
|
27
|
+
|
28
|
+
expect(
|
29
|
+
dynamodb.list_tables.table_names.include? "Testy#{count}_1"
|
30
|
+
).to be_true
|
31
|
+
end
|
32
|
+
|
33
|
+
it "creates as many copies of a table as requested" do
|
34
|
+
new_tables = dyna.create_table "Testie#{count}", 5
|
35
|
+
|
36
|
+
expect(
|
37
|
+
dynamodb.list_tables.table_names.include? "Testie#{count}_1"
|
38
|
+
).to be_true
|
39
|
+
|
40
|
+
expect(
|
41
|
+
dynamodb.list_tables.table_names.include? "Testie#{count}_5"
|
42
|
+
).to be_true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#explode" do
|
47
|
+
it "returns a DynamoDB-flavored hash given a plain hash" do
|
48
|
+
expect(
|
49
|
+
dyna.explode({ "foo" => "bar" })
|
50
|
+
).to eq({"foo" => { s: "bar" } })
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#get_item" do
|
55
|
+
it "gets an item" do
|
56
|
+
dynamodb.put_item table_name: table.table_name, item: dynamo_item
|
57
|
+
result = dyna.get_item [table.table_name], dynamo_item
|
58
|
+
|
59
|
+
expect( result["hash"]["s"] ).to eq("test#{count}")
|
60
|
+
end
|
61
|
+
|
62
|
+
it "gets an item across multiple tables" do
|
63
|
+
dynamodb.put_item table_name: table.table_name, item: dynamo_item
|
64
|
+
empty_table = dyna.create_table "Testy#{count}_empty"
|
65
|
+
result = dyna.get_item [table.table_name, "Testy#{count}_empty_1"], dynamo_item
|
66
|
+
|
67
|
+
expect( result["hash"]["s"] ).to eq("test#{count}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "#implode" do
|
72
|
+
it "returns a plain hash given a DynamoDB-flavored hash" do
|
73
|
+
expect(
|
74
|
+
dyna.implode({"foo" => { s: "bar" } })
|
75
|
+
).to eq({ "foo" => "bar" })
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#find" do
|
80
|
+
it "finds a table" do
|
81
|
+
dyna.create_table "Testy#{count}"
|
82
|
+
|
83
|
+
expect(
|
84
|
+
dyna.find("Testy#{count}_1").table.table_name
|
85
|
+
).to match("Testy#{count}")
|
86
|
+
end
|
87
|
+
|
88
|
+
it "returns false if it does not find a table" do
|
89
|
+
expect(
|
90
|
+
dyna.find "nope"
|
91
|
+
).to be_false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "#put_item" do
|
96
|
+
it "puts an item" do
|
97
|
+
dyna.put_item [table.table_name], item
|
98
|
+
|
99
|
+
expect(
|
100
|
+
dynamodb.get_item(table_name: table.table_name, key: dynamo_item).item["hash"]["s"]
|
101
|
+
).to eq("test#{count}")
|
102
|
+
end
|
103
|
+
|
104
|
+
it "puts an item to multiple tables" do
|
105
|
+
mirror_tables = dyna.create_table "Testie#{count}", 2
|
106
|
+
dyna.put_item ["Testie#{count}_1", "Testie#{count}_2"], item
|
107
|
+
|
108
|
+
expect(
|
109
|
+
dynamodb.get_item(table_name: "Testie#{count}_1", key: dynamo_item).item["hash"]["s"]
|
110
|
+
).to eq(dynamodb.get_item(table_name: "Testie#{count}_2", key: dynamo_item).item["hash"]["s"])
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
describe Moe::ModuleFunctions do
|
2
|
+
let!(:setup) { Moe::Sequence.setup "name", 2, 5, 10 }
|
3
|
+
|
4
|
+
describe ".collection" do
|
5
|
+
it "is a convenience method returning a Moe::Sequence::Collection" do
|
6
|
+
collection = Moe.collection "name", "owner"
|
7
|
+
|
8
|
+
expect( collection ).to be_a(Moe::Sequence::Collection)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe ".collector" do
|
13
|
+
it "is a convenience method returning a Moe::Sequence::Collector" do
|
14
|
+
collector = Moe.collector "name", "owner"
|
15
|
+
|
16
|
+
expect( collector ).to be_a(Moe::Sequence::Collector)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|