Permalink
Browse files

Optionally wait until validation request is complete, and add robust …

…handling of TooManyInvalidationsInProgress errors
  • Loading branch information...
1 parent 8bde9cc commit e8185f93ada1938d76d32167254710e2cfdc7a99 Jacob Elder committed May 24, 2012
Showing with 85 additions and 28 deletions.
  1. +13 −8 README.md
  2. +4 −1 bin/cloudfront-invalidator
  3. +68 −19 lib/cloudfront-invalidator.rb
View
@@ -1,13 +1,18 @@
Usage
=====
- invalidator = CloudfrontInvalidator.new('<AWS key>', '<AWS secret>', '<CF distribution id>')
- invalidator.invalidate('/images/rails.png', '/images/example.jpg')
- invalidator.list()
- invalidator.list_detail()
+ invalidator = CloudfrontInvalidator.new(AWS_KEY, AWS_SECRET, CF_DISTRIBUTION_ID)
+ list = %w[
+ index.html
+ favicon.ico
+ ]
+ invalidator.invalidate(list) do |status,time| # Block is optional.
+ invalidator.list # Or invalidator.list_detail
+ puts "Complete after < #{time.to_f.ceil} seconds." if status == "Complete"
+ end
-or, from the command line:
+A command line utility is also included.
-`cloudfront-invalidator invalidate aws_key aws_secret distribution_id /images/rails.png /images/example.jpg`
-`cloudfront-invalidator list aws_key aws_secret distribution_id`
-`cloudfront-invalidator list_detail aws_key aws_secret distribution_id`
+ $ cloudfront-invalidator invalidate $AWS_KEY $AWS_SECRET $DISTRIBUTION_ID index.html favicon.ico
+ $ cloudfront-invalidator list $AWS_KEY $AWS_SECRET $DISTRIBUTION_ID
+ $ cloudfront-invalidator list_detail $AWS_KEY $AWS_SECRET $DISTRIBUTION_ID
@@ -22,7 +22,10 @@ paths = ARGV
invalidator = CloudfrontInvalidator.new(aws_key, aws_secret, distribution_id)
case verb
when 'invalidate'
- invalidator.invalidate(paths)
+ print "Invalidating #{paths.size} objects"
+ invalidator.invalidate(paths) do |status,time|
+ print status == "Complete" ? "\nComplete after < #{time.to_f.ceil} seconds.\n" : "."
+ end
when 'list'
invalidator.list
when 'list_detail'
@@ -4,38 +4,80 @@
require 'hmac-sha1' # this is a gem
class CloudfrontInvalidator
+ API_VERSION = '2012-05-05'
+ BASE_URL = "https://cloudfront.amazonaws.com/#{API_VERSION}/distribution/"
+ DOC_URL = "http://cloudfront.amazonaws.com/doc/#{API_VERSION}/"
+ BACKOFF_LIMIT = 8192
+ BACKOFF_DELAY = 0.025
def initialize(aws_key, aws_secret, cf_dist_id)
@aws_key, @aws_secret, @cf_dist_id = aws_key, aws_secret, cf_dist_id
-
- @BASE_URL = "https://cloudfront.amazonaws.com/2010-11-01/distribution/"
end
def invalidate(*keys)
keys = keys.flatten.map do |k|
k.start_with?('/') ? k : '/' + k
end
- uri = URI.parse "#{@BASE_URL}#{@cf_dist_id}/invalidation"
+ uri = URI.parse "#{BASE_URL}#{@cf_dist_id}/invalidation"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
body = xml_body(keys)
- resp = http.send_request 'POST', uri.path, body, headers
- puts resp.code
- puts resp.body
+
+ delay = 1
+ begin
+ resp = http.send_request 'POST', uri.path, body, headers
+ doc = REXML::Document.new resp.body
+
+ # Create and raise an exception for any error the API returns to us.
+ if resp.code.to_i != 201
+ error_code = doc.elements["ErrorResponse/Error/Code"].text
+ self.class.const_set(error_code,Class.new(StandardError)) unless self.class.const_defined?(error_code.to_sym)
+ raise self.class.const_get(error_code).new(doc.elements["ErrorResponse/Error/Message"].text)
+ end
+
+ # Handle the common case of too many in progress by waiting until the others finish.
+ rescue TooManyInvalidationsInProgress => e
+ sleep delay * BACKOFF_DELAY
+ delay *= 2 unless delay >= BACKOFF_LIMIT
+ STDERR.puts e.inspect
+ retry
+ end
+
+ # If we are passed a block, poll on the status of this invalidation with truncated exponential backoff.
+ if block_given?
+ invalidation_id = doc.elements["Invalidation/Id"].text
+ poll_invalidation(invalidation_id) do |status,time|
+ yield status, time
+ end
+ end
+ return resp
end
-
+
+ def poll_invalidation(invalidation_id)
+ start = Time.now
+ delay = 1
+ loop do
+ doc = REXML::Document.new get_invalidation_detail_xml(invalidation_id)
+ status = doc.elements["Invalidation/Status"].text
+ yield status, Time.now - start
+ break if status != "InProgress"
+ sleep delay * BACKOFF_DELAY
+ delay *= 2 unless delay >= BACKOFF_LIMIT
+ end
+ end
+
def list(show_detail = false)
- uri = URI.parse "#{@BASE_URL}#{@cf_dist_id}/invalidation"
+ uri = URI.parse "#{BASE_URL}#{@cf_dist_id}/invalidation"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
resp = http.send_request 'GET', uri.path, '', headers
doc = REXML::Document.new resp.body
puts "MaxItems " + doc.elements["InvalidationList/MaxItems"].text + "; " + (doc.elements["InvalidationList/MaxItems"].text == "true" ? "truncated" : "not truncated")
-
+
doc.each_element("/InvalidationList/InvalidationSummary") do |summary|
invalidation_id = summary.elements["Id"].text
summary_text = "ID " + invalidation_id + ": " + summary.elements["Status"].text
@@ -56,27 +98,32 @@ def list(show_detail = false)
end
end
end
-
+
def list_detail
list(true)
end
-
+
def get_invalidation_detail_xml(invalidation_id)
- uri = URI.parse "#{@BASE_URL}#{@cf_dist_id}/invalidation/#{invalidation_id}"
+ uri = URI.parse "#{BASE_URL}#{@cf_dist_id}/invalidation/#{invalidation_id}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
resp = http.send_request 'GET', uri.path, '', headers
return resp.body
end
-
+
def xml_body(keys)
xml = <<XML
<?xml version="1.0" encoding="UTF-8"?>
- <InvalidationBatch>
- #{keys.map{|k| "<Path>#{k}</Path>" }.join("\n ")}
- <CallerReference>CloudfrontInvalidator at #{Time.now}</CallerReference>"
- </InvalidationBatch>
+<InvalidationBatch xmlns="#{DOC_URL}">
+ <Paths>
+ <Quantity>#{keys.size}</Quantity>
+ <Items>
+ #{keys.map{|k| "<Path>#{k}</Path>" }.join("\n ")}
+ </Items>
+ </Paths>
+ <CallerReference>#{self.class.to_s} on #{Socket.gethostname} at #{Time.now.to_i}</CallerReference>"
+</InvalidationBatch>
XML
end
@@ -87,5 +134,7 @@ def headers
signature = Base64.encode64(digest.digest)
{'Date' => date, 'Authorization' => "AWS #{@aws_key}:#{signature}"}
end
-
-end
+
+ class TooManyInvalidationsInProgress < StandardError ; end
+
+end

0 comments on commit e8185f9

Please sign in to comment.