diff --git a/.travis.yml b/.travis.yml index 21cb818..5cdebc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ language: ruby +before_install: + - gem install bundler -v 1.9.10 rvm: - 2.1 - 2.0.0 - 1.9.3 + - 2.3.1 script: bundle exec rspec diff --git a/lib/tina/cli.rb b/lib/tina/cli.rb index d7c5967..2ad29ba 100644 --- a/lib/tina/cli.rb +++ b/lib/tina/cli.rb @@ -16,9 +16,9 @@ def restore(prefix_file) prefixes = File.readlines(prefix_file).map(&:chomp) objects = RestorePlan::ObjectCollection.new(s3_client.list_bucket_prefixes(prefixes)) - restore_plan = RestorePlan.new(total_storage.to_i, objects) - price = restore_plan.price(duration_in_seconds) - chunks = objects.chunk(duration_in_seconds) + restore_plan = RestorePlan.new(total_storage.to_i, objects, duration_in_seconds) + price = restore_plan.price + chunks = restore_plan.object_chunks say say "Restores will be performed in the following chunks:" say "-" * 60 diff --git a/lib/tina/restore_plan.rb b/lib/tina/restore_plan.rb index ea3d988..88a5499 100644 --- a/lib/tina/restore_plan.rb +++ b/lib/tina/restore_plan.rb @@ -5,20 +5,18 @@ class RestorePlan DAYS_PER_MONTH = 30 PRICE_PER_GB_PER_HOUR = 0.011 - def initialize(total_storage_size, objects, options = {}) + def initialize(total_storage_size, objects, total_time, options = {}) @total_storage_size = total_storage_size @objects = objects + @total_time = [total_time, 4 * 3600].max @price_per_gb_per_hour = options[:price_per_gb_per_hour] || PRICE_PER_GB_PER_HOUR @daily_allowance = @total_storage_size * MONTHLY_FREE_TIER_ALLOWANCE_FACTOR / DAYS_PER_MONTH end - def price(total_time) - total_time = [total_time, 4 * 3600].max - chunk_size = quadhourly_restore_rate(total_time) - chunks = @objects.chunk(chunk_size) - largest_chunk_object_size = chunks.map { |chunk| chunk.map(&:size).reduce(&:+) }.max - quadhours = chunks.size + def price + largest_chunk_object_size = object_chunks.map { |chunk| chunk.map(&:size).reduce(&:+) }.max + quadhours = object_chunks.size quadhourly_allowance = @daily_allowance / ( [(24 / 4), quadhours].min * 4) peak_retrieval_rate = largest_chunk_object_size / 4 @@ -26,10 +24,15 @@ def price(total_time) peak_billable_retrieval_rate * (@price_per_gb_per_hour / 1024 ** 3) * 720 end + + def object_chunks + @object_chunks ||= @objects.chunk(quadhourly_restore_rate) + end + private - def quadhourly_restore_rate(total_time) - @objects.total_size / (total_time / (4 * 3600)) + def quadhourly_restore_rate + @objects.total_size / (@total_time / (4 * 3600)) end class ObjectCollection @@ -45,17 +48,17 @@ def size end def chunk(max_chunk_size) - @chunks ||= begin - chunks = @objects.chunk(sum: 0, index: 0) do |object, state| - state[:sum] += object.size - if state[:sum] > max_chunk_size - state[:sum] = object.size - state[:index] += 1 - end - state[:index] + sum = 0 + index = 0 + chunks = @objects.chunk do |object| + sum += object.size + if sum > max_chunk_size + sum = object.size + index += 1 end - chunks.map(&:last) + index end + chunks.map(&:last) end end end diff --git a/spec/tina/restore_plan_spec.rb b/spec/tina/restore_plan_spec.rb index 45adb8e..aca37d7 100644 --- a/spec/tina/restore_plan_spec.rb +++ b/spec/tina/restore_plan_spec.rb @@ -2,10 +2,6 @@ module Tina describe RestorePlan do - subject do - described_class.new(total_storage_size, object_collection, options) - end - let :total_storage_size do 75 * (1024 ** 4) end @@ -29,15 +25,15 @@ module Tina # http://aws.amazon.com/glacier/faqs/ context 'with the examples given on the Amazon Glacier pricing FAQ' do it 'matches the the price for a restore with everything at once' do - expect(subject.price(4 * 3600)).to be_within(0.05).of(21.6) + expect(described_class.new(total_storage_size, object_collection, 4 * 3600, options).price).to be_within(0.05).of(21.6) end it 'matches the the price for a restore over 8 hours' do - expect(subject.price(8 * 3600)).to be_within(0.05).of(10.8) + expect(described_class.new(total_storage_size, object_collection, 8 * 3600, options).price).to be_within(0.05).of(10.8) end it 'matches the the price for a restore over 28 hours' do - expect(subject.price(28 * 3600)).to eq 0 + expect(described_class.new(total_storage_size, object_collection, 28 * 3600, options).price).to eq 0 end end @@ -58,19 +54,19 @@ module Tina end it 'matches the price for a restore over a month' do - expect(subject.price(30 * 24 * 3600)).to be_within(0.05).of(4.16) + expect(described_class.new(total_storage_size, object_collection, 30 * 24 * 3600, options).price).to be_within(0.05).of(4.16) end it 'matches the price for a restore over a week' do - expect(subject.price(7 * 24 * 3600)).to be_within(0.05).of(437.87) + expect(described_class.new(total_storage_size, object_collection, 7 * 24 * 3600, options).price).to be_within(0.05).of(437.87) end it 'matches the price for a restore over a day' do - expect(subject.price(1 * 24 * 3600)).to be_within(0.05).of(3832.16) + expect(described_class.new(total_storage_size, object_collection, 1 * 24 * 3600, options).price).to be_within(0.05).of(3832.16) end it 'matches the price for a restore over a 4 hour period' do - expect(subject.price(4 * 3600)).to be_within(0.05).of(22992.93) + expect(described_class.new(total_storage_size, object_collection, 4 * 3600, options).price).to be_within(0.05).of(22992.93) end end @@ -84,12 +80,37 @@ module Tina end it 'matches the price for a restore over 4 days' do - expect(subject.price(4 * 24 * 3600)).to be_within(20).of(768) + expect(described_class.new(total_storage_size, object_collection, 4 * 24 * 3600, options).price).to be_within(20).of(768) end end end end end + + describe RestorePlan::ObjectCollection do + describe '#chunk' do + let :object_collection do + described_class.new(objects) + end + + let :objects do + [ + double(:fake_object1, size: 3), + double(:fake_object2, size: 3), + double(:fake_object3, size: 3), + ] + end + + it 'chunks objects into chunks of the maximum size given' do + expect(object_collection.chunk(6)).to eq([objects[0..1], objects[2..2]]) + end + + it 'places objects larger than the given maximum size into their own chunks' do + objects[1] = double(:large_object, size: 42) + expect(object_collection.chunk(6)).to eq([objects[0..0], objects[1..1], objects[2..2]]) + end + end + end end module SpecHelpers