middleman-s3_sync 4.6.0 → 4.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d5baff2d2863949eb480b7ac94d105d3fdcc2c7df67fab66c649f575dd686a4
4
- data.tar.gz: 70c12f42e60eabb0bf063d47a3e42f2766ea883bf47c17557cf323b15a9e546a
3
+ metadata.gz: ad34e26fb6ea816a0426ef33e543c4eabc3c5323151f1c40512a95183ae6ed7e
4
+ data.tar.gz: a5b875195ed49d7b311eb87a6672a67aba69773e9d85f50fc1022c185238cf41
5
5
  SHA512:
6
- metadata.gz: 663ab12a585095543adb3843976e1d24bbf5d3e6bb52a5285b3ce7de5cc7796cb916b72dc330d110225e191325add7e6eed81797abccae9c4e66d0b1d84c8608
7
- data.tar.gz: b9b720083616f701318747b661aa04d9257c9539136ae0a2d153cc314eced337baa7ca158cb6e10de1d86ef1bd29cb90cd49b7ee1160c69bdf7b9bab09d4c27f
6
+ metadata.gz: c8f1e1678fd3e45350cdc4efefdd3b2520ad90abd075478dbfcfabb85d8c6294aef1c78acffc7451a9f19cb69b890fd90f6049f35a3000960c87906c7188059f
7
+ data.tar.gz: 8b062b097077d16019a1aef109338ab3603941dc4a57d06ea0da23374fc5a947ac0f93b0bbedb08eac1f87e3fd1df38fd7db5f3c116d6a04f4c8bf7f79ee1c39
data/.s3_sync.sample CHANGED
@@ -20,4 +20,6 @@ cloudfront_distribution_id: <CloudFront Distribution ID> # e.g., E1234567890123
20
20
  cloudfront_invalidate: false # Set to true to enable
21
21
  cloudfront_invalidate_all: false # Set to true to invalidate all paths (/*)
22
22
  cloudfront_invalidation_batch_size: 1000 # Max paths per invalidation request
23
+ cloudfront_invalidation_max_retries: 5 # Max retries for rate-limited requests
24
+ cloudfront_invalidation_batch_delay: 2 # Delay in seconds between invalidation batches
23
25
  cloudfront_wait: false # Set to true to wait for invalidation to complete
data/Changelog.md CHANGED
@@ -2,6 +2,65 @@
2
2
 
3
3
  The gem that tries really hard not to push files to S3.
4
4
 
5
+ ## v4.6.1
6
+
7
+ * Add CloudFront rate limit handling with exponential backoff retry logic
8
+ * Add configurable retry settings: `cloudfront_invalidation_max_retries` and `cloudfront_invalidation_batch_delay`
9
+ * Improve CloudFront error handling for "Rate exceeded" and "Throttling" errors
10
+ * Add command line options for retry configuration
11
+ * Update documentation with retry configuration examples
12
+ * Increase default batch delay from 1 to 2 seconds for better rate limit prevention
13
+
14
+ ## v4.6.0
15
+
16
+ * Add comprehensive CloudFront invalidation support with smart path tracking
17
+ * Add CloudFront configuration options and command line switches
18
+ * Add batch processing for CloudFront invalidations to respect API limits
19
+ * Add path normalization and deduplication for efficient invalidations
20
+ * Add dry-run support for CloudFront invalidations
21
+ * Add wait functionality for CI/CD pipeline integration
22
+ * Update README with CloudFront documentation and best practices
23
+
24
+ ## v4.5.0
25
+
26
+ * Migrate from Fog gem to native AWS SDK S3 client
27
+ * Remove numerous transitive dependencies by dropping Fog
28
+ * Fix path handling inconsistencies with leading slashes
29
+ * Improve resource handling for AWS SDK v3 compatibility
30
+ * Add Ruby 3.2 compatibility fixes
31
+ * Fix build behavior to not auto-build unless explicitly requested
32
+ * Optimize resource processing for better performance
33
+ * Update nokogiri dependency for security
34
+ * Enhance test coverage and mocking
35
+
36
+ ## v4.4.0
37
+
38
+ * Add support for newer Ruby versions (3.0+)
39
+ * Update dependencies for security and compatibility
40
+ * Fix deprecation warnings with newer Ruby versions
41
+ * Improve error handling and logging
42
+
43
+ ## v4.3.0
44
+
45
+ * Enhanced S3 client configuration options
46
+ * Improved AWS credential handling
47
+ * Better support for custom S3 endpoints
48
+ * Performance optimizations for large sites
49
+
50
+ ## v4.2.0
51
+
52
+ * Add support for S3 transfer acceleration
53
+ * Improve concurrent upload handling
54
+ * Enhanced progress reporting
55
+ * Better error messages and debugging
56
+
57
+ ## v4.1.0
58
+
59
+ * Add support for custom content types
60
+ * Improve gzip handling and encoding detection
61
+ * Enhanced caching policy management
62
+ * Better support for redirects and metadata
63
+
5
64
  ## v4.0.1
6
65
 
7
66
  * Fix order of manipulator chain so that S3 Sync is always the last action
data/README.md CHANGED
@@ -172,14 +172,15 @@ end
172
172
 
173
173
  ### Configuration Options
174
174
 
175
- | Setting | Default | Description |
176
- | --------------------------------- | ----------- | ----------- |
177
- | cloudfront_distribution_id | - | CloudFront distribution ID to invalidate |
178
- | cloudfront_invalidate | ```false``` | Enable CloudFront invalidation after sync |
179
- | cloudfront_invalidate_all | ```false``` | Invalidate all paths (/*) instead of only changed files |
180
- | cloudfront_invalidation_batch_size| ```1000``` | Maximum paths per invalidation request |
181
- | cloudfront_wait | ```false``` | Wait for CloudFront invalidation to complete |
182
- </edits>
175
+ | Setting | Default | Description |
176
+ | ------------------------------------- | ----------- | ----------- |
177
+ | cloudfront_distribution_id | - | CloudFront distribution ID to invalidate |
178
+ | cloudfront_invalidate | ```false``` | Enable CloudFront invalidation after sync |
179
+ | cloudfront_invalidate_all | ```false``` | Invalidate all paths (/*) instead of only changed files |
180
+ | cloudfront_invalidation_batch_size | ```1000``` | Maximum paths per invalidation request |
181
+ | cloudfront_invalidation_max_retries | ```5``` | Maximum retries for rate-limited requests |
182
+ | cloudfront_invalidation_batch_delay | ```2``` | Delay in seconds between invalidation batches |
183
+ | cloudfront_wait | ```false``` | Wait for CloudFront invalidation to complete |
183
184
 
184
185
  ### Command Line Options
185
186
 
@@ -198,6 +199,9 @@ middleman s3_sync --cloudfront-invalidate --cloudfront-wait --cloudfront-distrib
198
199
  # Custom batch size for large numbers of files
199
200
  middleman s3_sync --cloudfront-invalidate --cloudfront-invalidation-batch-size 500 --cloudfront-distribution-id E1234567890123
200
201
 
202
+ # Adjust retry behavior for rate limiting
203
+ middleman s3_sync --cloudfront-invalidate --cloudfront-invalidation-max-retries 3 --cloudfront-invalidation-batch-delay 5 --cloudfront-distribution-id E1234567890123
204
+
201
205
  # Short aliases
202
206
  middleman s3_sync -c -d E1234567890123 # Basic invalidation
203
207
  middleman s3_sync -a -d E1234567890123 # Invalidate all paths
@@ -213,6 +217,8 @@ middleman s3_sync -c -a -w -d E1234567890123 # Invalidate all and wait
213
217
  | `--cloudfront-invalidate` | `-c` | Enable CloudFront invalidation |
214
218
  | `--cloudfront-invalidate-all` | `-a` | Invalidate all paths (/*) |
215
219
  | `--cloudfront-invalidation-batch-size` | - | Max paths per request (default: 1000) |
220
+ | `--cloudfront-invalidation-max-retries` | - | Max retries for rate limits (default: 5) |
221
+ | `--cloudfront-invalidation-batch-delay` | - | Delay between batches in seconds (default: 2) |
216
222
  | `--cloudfront-wait` | `-w` | Wait for invalidation to complete |
217
223
 
218
224
  ### How It Works
@@ -220,8 +226,9 @@ middleman s3_sync -c -a -w -d E1234567890123 # Invalidate all and wait
220
226
  1. **Smart Invalidation**: By default, only files that were created, updated, or deleted during the sync are invalidated
221
227
  2. **Path Optimization**: Duplicate paths are removed and redundant paths (covered by wildcards) are eliminated
222
228
  3. **Batch Processing**: Large numbers of paths are split into multiple invalidation requests to respect CloudFront limits
223
- 4. **Error Handling**: Invalidation failures are logged but don't stop the sync process
224
- 5. **Dry Run Support**: Use `--dry-run` to see what would be invalidated without making actual API calls
229
+ 4. **Rate Limit Handling**: Automatic retry with exponential backoff when CloudFront rate limits are hit
230
+ 5. **Error Handling**: Invalidation failures are logged but don't stop the sync process
231
+ 6. **Dry Run Support**: Use `--dry-run` to see what would be invalidated without making actual API calls
225
232
 
226
233
  ### IAM Permissions
227
234
 
@@ -31,11 +31,12 @@ module Middleman
31
31
  path_batches.each_with_index do |batch, index|
32
32
  say_status "Creating invalidation batch #{index + 1}/#{path_batches.length} (#{batch.length} paths)"
33
33
 
34
- invalidation_id = create_invalidation(batch, options)
34
+ invalidation_id = create_invalidation_with_retry(batch, options)
35
35
  invalidation_ids << invalidation_id if invalidation_id
36
36
 
37
- # Add a small delay between batches to avoid rate limiting
38
- sleep(1) if path_batches.length > 1 && index < path_batches.length - 1
37
+ # Add a delay between batches to avoid rate limiting
38
+ delay = options.cloudfront_invalidation_batch_delay || 2
39
+ sleep(delay) if path_batches.length > 1 && index < path_batches.length - 1
39
40
  end
40
41
 
41
42
  if invalidation_ids.any?
@@ -119,6 +120,29 @@ module Middleman
119
120
  result
120
121
  end
121
122
 
123
+ def create_invalidation_with_retry(paths, options)
124
+ max_retries = options.cloudfront_invalidation_max_retries || 5
125
+ retries = 0
126
+ base_delay = 1
127
+
128
+ begin
129
+ create_invalidation(paths, options)
130
+ rescue Aws::CloudFront::Errors::ServiceError => e
131
+ if (e.message.include?('Rate exceeded') || e.message.include?('Throttling')) && retries < max_retries
132
+ retries += 1
133
+ delay = base_delay * (2 ** (retries - 1)) + rand(1..3) # Exponential backoff with jitter
134
+ say_status "#{ANSI.yellow{"Rate limit hit, retrying in #{delay} seconds..."}} (attempt #{retries}/#{max_retries})"
135
+ sleep(delay)
136
+ retry
137
+ else
138
+ say_status "#{ANSI.red{'Failed to create CloudFront invalidation:'}} #{e.message}"
139
+ say_status "Paths: #{paths.join(', ')}" if options.verbose
140
+ raise e unless options.verbose
141
+ nil
142
+ end
143
+ end
144
+ end
145
+
122
146
  def create_invalidation(paths, options)
123
147
  caller_reference = "middleman-s3_sync-#{Time.now.to_i}-#{SecureRandom.hex(4)}"
124
148
 
@@ -134,11 +158,6 @@ module Middleman
134
158
  })
135
159
 
136
160
  response.invalidation.id
137
- rescue Aws::CloudFront::Errors::ServiceError => e
138
- say_status "#{ANSI.red{'Failed to create CloudFront invalidation:'}} #{e.message}"
139
- say_status "Paths: #{paths.join(', ')}" if options.verbose
140
- raise e unless options.verbose
141
- nil
142
161
  end
143
162
 
144
163
  def cloudfront_client(options)
@@ -1,5 +1,5 @@
1
1
  module Middleman
2
2
  module S3Sync
3
- VERSION = "4.6.0"
3
+ VERSION = "4.6.1"
4
4
  end
5
5
  end
@@ -84,6 +84,14 @@ module Middleman
84
84
  type: :numeric,
85
85
  desc: 'Maximum number of paths to invalidate in a single request (default: 1000).'
86
86
 
87
+ class_option :cloudfront_invalidation_max_retries,
88
+ type: :numeric,
89
+ desc: 'Maximum number of retries for rate-limited invalidation requests (default: 5).'
90
+
91
+ class_option :cloudfront_invalidation_batch_delay,
92
+ type: :numeric,
93
+ desc: 'Delay in seconds between invalidation batches (default: 2).'
94
+
87
95
  class_option :cloudfront_wait,
88
96
  aliases: '-w',
89
97
  type: :boolean,
@@ -127,6 +135,8 @@ module Middleman
127
135
  s3_sync_options.cloudfront_invalidate = options[:cloudfront_invalidate] if options[:cloudfront_invalidate]
128
136
  s3_sync_options.cloudfront_invalidate_all = options[:cloudfront_invalidate_all] if options[:cloudfront_invalidate_all]
129
137
  s3_sync_options.cloudfront_invalidation_batch_size = options[:cloudfront_invalidation_batch_size] if options[:cloudfront_invalidation_batch_size]
138
+ s3_sync_options.cloudfront_invalidation_max_retries = options[:cloudfront_invalidation_max_retries] if options[:cloudfront_invalidation_max_retries]
139
+ s3_sync_options.cloudfront_invalidation_batch_delay = options[:cloudfront_invalidation_batch_delay] if options[:cloudfront_invalidation_batch_delay]
130
140
  s3_sync_options.cloudfront_wait = options[:cloudfront_wait] if options[:cloudfront_wait]
131
141
 
132
142
  ::Middleman::S3Sync.sync()
@@ -33,6 +33,8 @@ module Middleman
33
33
  option :cloudfront_invalidate, false, 'Whether to invalidate CloudFront cache after sync'
34
34
  option :cloudfront_invalidate_all, false, 'Whether to invalidate all paths (/*) or only changed files'
35
35
  option :cloudfront_invalidation_batch_size, 1000, 'Maximum number of paths to invalidate in a single request'
36
+ option :cloudfront_invalidation_max_retries, 5, 'Maximum number of retries for rate-limited invalidation requests'
37
+ option :cloudfront_invalidation_batch_delay, 2, 'Delay in seconds between invalidation batches'
36
38
  option :cloudfront_wait, false, 'Whether to wait for CloudFront invalidation to complete'
37
39
 
38
40
  expose_to_config :s3_sync_options, :default_caching_policy, :caching_policy
@@ -8,6 +8,8 @@ describe Middleman::S3Sync::CloudFront do
8
8
  cloudfront_distribution_id: 'E1234567890123',
9
9
  cloudfront_invalidate_all: false,
10
10
  cloudfront_invalidation_batch_size: 1000,
11
+ cloudfront_invalidation_max_retries: 5,
12
+ cloudfront_invalidation_batch_delay: 2,
11
13
  cloudfront_wait: false,
12
14
  aws_access_key_id: 'test_key',
13
15
  aws_secret_access_key: 'test_secret',
@@ -92,6 +94,8 @@ describe Middleman::S3Sync::CloudFront do
92
94
  cloudfront_distribution_id: 'E1234567890123',
93
95
  cloudfront_invalidate_all: true,
94
96
  cloudfront_invalidation_batch_size: 1000,
97
+ cloudfront_invalidation_max_retries: 5,
98
+ cloudfront_invalidation_batch_delay: 2,
95
99
  cloudfront_wait: false,
96
100
  aws_access_key_id: 'test_key',
97
101
  aws_secret_access_key: 'test_secret',
@@ -201,6 +205,8 @@ describe Middleman::S3Sync::CloudFront do
201
205
  cloudfront_distribution_id: 'E1234567890123',
202
206
  cloudfront_invalidate_all: false,
203
207
  cloudfront_invalidation_batch_size: 2,
208
+ cloudfront_invalidation_max_retries: 5,
209
+ cloudfront_invalidation_batch_delay: 1,
204
210
  cloudfront_wait: false,
205
211
  aws_access_key_id: 'test_key',
206
212
  aws_secret_access_key: 'test_secret',
@@ -239,6 +245,112 @@ describe Middleman::S3Sync::CloudFront do
239
245
  }.to raise_error(Aws::CloudFront::Errors::ServiceError)
240
246
  end
241
247
 
248
+ context 'when rate limit is exceeded' do
249
+ let(:rate_error) { Aws::CloudFront::Errors::ServiceError.new(nil, 'Rate exceeded') }
250
+
251
+ it 'retries with exponential backoff' do
252
+ client = double('cloudfront_client')
253
+ allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
254
+
255
+ # Fail twice with rate limit, then succeed
256
+ call_count = 0
257
+ allow(client).to receive(:create_invalidation) do
258
+ call_count += 1
259
+ if call_count <= 2
260
+ raise rate_error
261
+ else
262
+ invalidation_response
263
+ end
264
+ end
265
+
266
+ # Allow normal status messages but expect retry messages
267
+ allow(described_class).to receive(:say_status)
268
+ expect(described_class).to receive(:say_status).with(
269
+ match(/Rate limit hit, retrying in \d+ seconds.*attempt 1\/5/)
270
+ ).ordered
271
+ expect(described_class).to receive(:say_status).with(
272
+ match(/Rate limit hit, retrying in \d+ seconds.*attempt 2\/5/)
273
+ ).ordered
274
+
275
+ # Expect sleep calls for backoff
276
+ expect(described_class).to receive(:sleep).twice
277
+
278
+ result = described_class.invalidate(['/path1'], options)
279
+ expect(result).to eq(['I1234567890123'])
280
+ end
281
+
282
+ it 'gives up after max retries and raises error' do
283
+ rate_limited_options = double(
284
+ cloudfront_invalidate: true,
285
+ cloudfront_distribution_id: 'E1234567890123',
286
+ cloudfront_invalidate_all: false,
287
+ cloudfront_invalidation_batch_size: 1000,
288
+ cloudfront_invalidation_max_retries: 2,
289
+ cloudfront_invalidation_batch_delay: 2,
290
+ cloudfront_wait: false,
291
+ aws_access_key_id: 'test_key',
292
+ aws_secret_access_key: 'test_secret',
293
+ aws_session_token: nil,
294
+ dry_run: false,
295
+ verbose: false
296
+ )
297
+
298
+ client = double('cloudfront_client')
299
+ allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
300
+
301
+ # Fail max_retries + 1 times
302
+ expect(client).to receive(:create_invalidation).exactly(3).times.and_raise(rate_error)
303
+
304
+ # Allow normal status messages
305
+ allow(described_class).to receive(:say_status)
306
+
307
+ # Expect retry status messages
308
+ expect(described_class).to receive(:say_status).with(
309
+ match(/Rate limit hit, retrying in \d+ seconds.*attempt 1\/2/)
310
+ )
311
+ expect(described_class).to receive(:say_status).with(
312
+ match(/Rate limit hit, retrying in \d+ seconds.*attempt 2\/2/)
313
+ )
314
+ expect(described_class).to receive(:say_status).with(
315
+ match(/Failed to create CloudFront invalidation.*Rate exceeded/)
316
+ )
317
+
318
+ # Expect sleep calls for backoff
319
+ expect(described_class).to receive(:sleep).twice
320
+
321
+ expect {
322
+ described_class.invalidate(['/path1'], rate_limited_options)
323
+ }.to raise_error(Aws::CloudFront::Errors::ServiceError)
324
+ end
325
+
326
+ it 'handles throttling errors the same as rate exceeded' do
327
+ throttling_error = Aws::CloudFront::Errors::ServiceError.new(nil, 'Throttling: Request was throttled')
328
+
329
+ client = double('cloudfront_client')
330
+ allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
331
+
332
+ call_count = 0
333
+ allow(client).to receive(:create_invalidation) do
334
+ call_count += 1
335
+ if call_count == 1
336
+ raise throttling_error
337
+ else
338
+ invalidation_response
339
+ end
340
+ end
341
+
342
+ # Allow normal status messages but expect retry message
343
+ allow(described_class).to receive(:say_status)
344
+ expect(described_class).to receive(:say_status).with(
345
+ match(/Rate limit hit, retrying in \d+ seconds.*attempt 1\/5/)
346
+ ).ordered
347
+ expect(described_class).to receive(:sleep).once
348
+
349
+ result = described_class.invalidate(['/path1'], options)
350
+ expect(result).to eq(['I1234567890123'])
351
+ end
352
+ end
353
+
242
354
  context 'when verbose mode is enabled' do
243
355
  let(:options) do
244
356
  double(
@@ -246,6 +358,8 @@ describe Middleman::S3Sync::CloudFront do
246
358
  cloudfront_distribution_id: 'E1234567890123',
247
359
  cloudfront_invalidate_all: false,
248
360
  cloudfront_invalidation_batch_size: 1000,
361
+ cloudfront_invalidation_max_retries: 5,
362
+ cloudfront_invalidation_batch_delay: 2,
249
363
  cloudfront_wait: false,
250
364
  aws_access_key_id: 'test_key',
251
365
  aws_secret_access_key: 'test_secret',
@@ -285,6 +399,8 @@ describe Middleman::S3Sync::CloudFront do
285
399
  cloudfront_distribution_id: 'E1234567890123',
286
400
  cloudfront_invalidate_all: false,
287
401
  cloudfront_invalidation_batch_size: 1000,
402
+ cloudfront_invalidation_max_retries: 5,
403
+ cloudfront_invalidation_batch_delay: 2,
288
404
  cloudfront_wait: true,
289
405
  aws_access_key_id: 'test_key',
290
406
  aws_secret_access_key: 'test_secret',
@@ -336,6 +452,8 @@ describe Middleman::S3Sync::CloudFront do
336
452
  cloudfront_distribution_id: 'E1234567890123',
337
453
  cloudfront_invalidate_all: false,
338
454
  cloudfront_invalidation_batch_size: 1000,
455
+ cloudfront_invalidation_max_retries: 5,
456
+ cloudfront_invalidation_batch_delay: 2,
339
457
  cloudfront_wait: false,
340
458
  aws_access_key_id: 'test_key',
341
459
  aws_secret_access_key: 'test_secret',
@@ -367,6 +485,8 @@ describe Middleman::S3Sync::CloudFront do
367
485
  cloudfront_distribution_id: 'E1234567890123',
368
486
  cloudfront_invalidate_all: false,
369
487
  cloudfront_invalidation_batch_size: 1000,
488
+ cloudfront_invalidation_max_retries: 5,
489
+ cloudfront_invalidation_batch_delay: 2,
370
490
  cloudfront_wait: false,
371
491
  aws_access_key_id: nil,
372
492
  aws_secret_access_key: nil,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: middleman-s3_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.6.0
4
+ version: 4.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frederic Jean