Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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

…handling of TooManyInvalidationsInProgress errors
  • Loading branch information...
commit e8185f93ada1938d76d32167254710e2cfdc7a99 1 parent 8bde9cc
Jacob Elder jelder authored
21 README.md
Source Rendered
... ... @@ -1,13 +1,18 @@
1 1 Usage
2 2 =====
3 3
4   - invalidator = CloudfrontInvalidator.new('<AWS key>', '<AWS secret>', '<CF distribution id>')
5   - invalidator.invalidate('/images/rails.png', '/images/example.jpg')
6   - invalidator.list()
7   - invalidator.list_detail()
  4 + invalidator = CloudfrontInvalidator.new(AWS_KEY, AWS_SECRET, CF_DISTRIBUTION_ID)
  5 + list = %w[
  6 + index.html
  7 + favicon.ico
  8 + ]
  9 + invalidator.invalidate(list) do |status,time| # Block is optional.
  10 + invalidator.list # Or invalidator.list_detail
  11 + puts "Complete after < #{time.to_f.ceil} seconds." if status == "Complete"
  12 + end
8 13
9   -or, from the command line:
  14 +A command line utility is also included.
10 15
11   -`cloudfront-invalidator invalidate aws_key aws_secret distribution_id /images/rails.png /images/example.jpg`
12   -`cloudfront-invalidator list aws_key aws_secret distribution_id`
13   -`cloudfront-invalidator list_detail aws_key aws_secret distribution_id`
  16 + $ cloudfront-invalidator invalidate $AWS_KEY $AWS_SECRET $DISTRIBUTION_ID index.html favicon.ico
  17 + $ cloudfront-invalidator list $AWS_KEY $AWS_SECRET $DISTRIBUTION_ID
  18 + $ cloudfront-invalidator list_detail $AWS_KEY $AWS_SECRET $DISTRIBUTION_ID
5 bin/cloudfront-invalidator
@@ -22,7 +22,10 @@ paths = ARGV
22 22 invalidator = CloudfrontInvalidator.new(aws_key, aws_secret, distribution_id)
23 23 case verb
24 24 when 'invalidate'
25   - invalidator.invalidate(paths)
  25 + print "Invalidating #{paths.size} objects"
  26 + invalidator.invalidate(paths) do |status,time|
  27 + print status == "Complete" ? "\nComplete after < #{time.to_f.ceil} seconds.\n" : "."
  28 + end
26 29 when 'list'
27 30 invalidator.list
28 31 when 'list_detail'
87 lib/cloudfront-invalidator.rb
@@ -4,11 +4,14 @@
4 4 require 'hmac-sha1' # this is a gem
5 5
6 6 class CloudfrontInvalidator
  7 + API_VERSION = '2012-05-05'
  8 + BASE_URL = "https://cloudfront.amazonaws.com/#{API_VERSION}/distribution/"
  9 + DOC_URL = "http://cloudfront.amazonaws.com/doc/#{API_VERSION}/"
  10 + BACKOFF_LIMIT = 8192
  11 + BACKOFF_DELAY = 0.025
7 12
8 13 def initialize(aws_key, aws_secret, cf_dist_id)
9 14 @aws_key, @aws_secret, @cf_dist_id = aws_key, aws_secret, cf_dist_id
10   -
11   - @BASE_URL = "https://cloudfront.amazonaws.com/2010-11-01/distribution/"
12 15 end
13 16
14 17 def invalidate(*keys)
@@ -16,18 +19,57 @@ def invalidate(*keys)
16 19 k.start_with?('/') ? k : '/' + k
17 20 end
18 21
19   - uri = URI.parse "#{@BASE_URL}#{@cf_dist_id}/invalidation"
  22 + uri = URI.parse "#{BASE_URL}#{@cf_dist_id}/invalidation"
20 23 http = Net::HTTP.new(uri.host, uri.port)
21 24 http.use_ssl = true
22 25 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
23 26 body = xml_body(keys)
24   - resp = http.send_request 'POST', uri.path, body, headers
25   - puts resp.code
26   - puts resp.body
  27 +
  28 + delay = 1
  29 + begin
  30 + resp = http.send_request 'POST', uri.path, body, headers
  31 + doc = REXML::Document.new resp.body
  32 +
  33 + # Create and raise an exception for any error the API returns to us.
  34 + if resp.code.to_i != 201
  35 + error_code = doc.elements["ErrorResponse/Error/Code"].text
  36 + self.class.const_set(error_code,Class.new(StandardError)) unless self.class.const_defined?(error_code.to_sym)
  37 + raise self.class.const_get(error_code).new(doc.elements["ErrorResponse/Error/Message"].text)
  38 + end
  39 +
  40 + # Handle the common case of too many in progress by waiting until the others finish.
  41 + rescue TooManyInvalidationsInProgress => e
  42 + sleep delay * BACKOFF_DELAY
  43 + delay *= 2 unless delay >= BACKOFF_LIMIT
  44 + STDERR.puts e.inspect
  45 + retry
  46 + end
  47 +
  48 + # If we are passed a block, poll on the status of this invalidation with truncated exponential backoff.
  49 + if block_given?
  50 + invalidation_id = doc.elements["Invalidation/Id"].text
  51 + poll_invalidation(invalidation_id) do |status,time|
  52 + yield status, time
  53 + end
  54 + end
  55 + return resp
27 56 end
28   -
  57 +
  58 + def poll_invalidation(invalidation_id)
  59 + start = Time.now
  60 + delay = 1
  61 + loop do
  62 + doc = REXML::Document.new get_invalidation_detail_xml(invalidation_id)
  63 + status = doc.elements["Invalidation/Status"].text
  64 + yield status, Time.now - start
  65 + break if status != "InProgress"
  66 + sleep delay * BACKOFF_DELAY
  67 + delay *= 2 unless delay >= BACKOFF_LIMIT
  68 + end
  69 + end
  70 +
29 71 def list(show_detail = false)
30   - uri = URI.parse "#{@BASE_URL}#{@cf_dist_id}/invalidation"
  72 + uri = URI.parse "#{BASE_URL}#{@cf_dist_id}/invalidation"
31 73 http = Net::HTTP.new(uri.host, uri.port)
32 74 http.use_ssl = true
33 75 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
@@ -35,7 +77,7 @@ def list(show_detail = false)
35 77
36 78 doc = REXML::Document.new resp.body
37 79 puts "MaxItems " + doc.elements["InvalidationList/MaxItems"].text + "; " + (doc.elements["InvalidationList/MaxItems"].text == "true" ? "truncated" : "not truncated")
38   -
  80 +
39 81 doc.each_element("/InvalidationList/InvalidationSummary") do |summary|
40 82 invalidation_id = summary.elements["Id"].text
41 83 summary_text = "ID " + invalidation_id + ": " + summary.elements["Status"].text
@@ -56,27 +98,32 @@ def list(show_detail = false)
56 98 end
57 99 end
58 100 end
59   -
  101 +
60 102 def list_detail
61 103 list(true)
62 104 end
63   -
  105 +
64 106 def get_invalidation_detail_xml(invalidation_id)
65   - uri = URI.parse "#{@BASE_URL}#{@cf_dist_id}/invalidation/#{invalidation_id}"
  107 + uri = URI.parse "#{BASE_URL}#{@cf_dist_id}/invalidation/#{invalidation_id}"
66 108 http = Net::HTTP.new(uri.host, uri.port)
67 109 http.use_ssl = true
68 110 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
69 111 resp = http.send_request 'GET', uri.path, '', headers
70 112 return resp.body
71 113 end
72   -
  114 +
73 115 def xml_body(keys)
74 116 xml = <<XML
75 117 <?xml version="1.0" encoding="UTF-8"?>
76   - <InvalidationBatch>
77   - #{keys.map{|k| "<Path>#{k}</Path>" }.join("\n ")}
78   - <CallerReference>CloudfrontInvalidator at #{Time.now}</CallerReference>"
79   - </InvalidationBatch>
  118 +<InvalidationBatch xmlns="#{DOC_URL}">
  119 + <Paths>
  120 + <Quantity>#{keys.size}</Quantity>
  121 + <Items>
  122 + #{keys.map{|k| "<Path>#{k}</Path>" }.join("\n ")}
  123 + </Items>
  124 + </Paths>
  125 + <CallerReference>#{self.class.to_s} on #{Socket.gethostname} at #{Time.now.to_i}</CallerReference>"
  126 +</InvalidationBatch>
80 127 XML
81 128 end
82 129
@@ -87,5 +134,7 @@ def headers
87 134 signature = Base64.encode64(digest.digest)
88 135 {'Date' => date, 'Authorization' => "AWS #{@aws_key}:#{signature}"}
89 136 end
90   -
91   -end
  137 +
  138 + class TooManyInvalidationsInProgress < StandardError ; end
  139 +
  140 +end

0 comments on commit e8185f9

Please sign in to comment.
Something went wrong with that request. Please try again.