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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Moe
2
+ VERSION = "0.0.1"
3
+ end
@@ -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