aws-sdk-s3 1.233.0 → 1.233.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: 39ab335ff541d8651defd1a742def7371faeabe89c6a3aeafef98e7130c40e56
4
- data.tar.gz: 6215b21914e2d2a008a269c4cbcc28745d9d8e0b5e27eeaf77f063723060a107
3
+ metadata.gz: 2a94f968574a4140131a94ed10f6e238db29a2cb3ceac5eaab1bccdda34efa4f
4
+ data.tar.gz: 63bbf8c2512e03438f6e22c634bb9070cc52e0b06096d7a6945dc55100303631
5
5
  SHA512:
6
- metadata.gz: 973291423658781b53f54c6d54d3d5a836f605b9169b9963daae754870e695b88850e573e1682ec872c4af984205653c50673c95a24e2aabfb8103ed3bdf1c04
7
- data.tar.gz: 1386d963bb662bacfddc26dbc4aa9bebb43d7aab54c9abbc7306e95c1ebf47d2b057f6ac1b0b6f4a411da77450170c91a560988f91cf33bd79defb1fd8b1c471
6
+ metadata.gz: 46c2a374e3f4bbd446d32b3403f08e2b648f37dc5f1cfb1d1c2224a211b43e6450ebdaa9cc3bbf9c6fa3e2a5b0884671127baf13163391fb11647a339f34ae5f
7
+ data.tar.gz: 564b1d9d422faa9180efc8e86f31803c517335608ff98eb05fa328094cc5de1a23cd22f13afeb09aa3e84aeb5f0e0f3b8a2e9a2f69437403efcbc3aa7bb2ebb2
data/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
3
 
4
+ 1.233.1 (2026-10-01)
5
+ ------------------
6
+
7
+ * Issue - Fix unbounded memory growth in `upload_stream` on `TransferManager` and `Aws::S3::Object` when the data source outpaces the upload (#3407).
8
+
9
+ * Issue - Prevent `upload_file`, `upload_stream` and `download_file` on `TransferManager` and `Aws::S3::Object` from hanging when a worker thread is terminated by a non-`StandardError`.
10
+
4
11
  1.233.0 (2026-09-30)
5
12
  ------------------
6
13
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.233.0
1
+ 1.233.1
@@ -24209,7 +24209,7 @@ module Aws::S3
24209
24209
  tracer: tracer
24210
24210
  )
24211
24211
  context[:gem_name] = 'aws-sdk-s3'
24212
- context[:gem_version] = '1.233.0'
24212
+ context[:gem_version] = '1.233.1'
24213
24213
  Seahorse::Client::Request.new(handlers, context)
24214
24214
  end
24215
24215
 
@@ -380,7 +380,9 @@ module Aws
380
380
  # and {Client#upload_part} can be provided.
381
381
  #
382
382
  # @option options [Integer] :thread_count (10) The number of parallel multipart uploads.
383
- # An additional thread is used internally for task coordination.
383
+ # An additional thread is used internally for task coordination. This also bounds
384
+ # how many parts are buffered ahead of the upload, limiting memory usage to roughly
385
+ # `2 * :thread_count * :part_size`.
384
386
  #
385
387
  # @option options [Boolean] :tempfile (false) Normally read data is stored
386
388
  # in memory when building the parts in order to complete the underlying
@@ -405,7 +407,8 @@ module Aws
405
407
  # @see Client#upload_part
406
408
  def upload_stream(options = {}, &block)
407
409
  upload_opts = options.merge(bucket: bucket_name, key: key)
408
- executor = DefaultExecutor.new(max_threads: upload_opts.delete(:thread_count))
410
+ thread_count = upload_opts.delete(:thread_count) || DefaultExecutor::DEFAULT_MAX_THREADS
411
+ executor = DefaultExecutor.new(max_threads: thread_count, max_queue: thread_count)
409
412
  begin
410
413
  uploader = MultipartStreamUploader.new(
411
414
  client: client,
@@ -4,6 +4,13 @@ module Aws
4
4
  module S3
5
5
  # @api private
6
6
  class DefaultExecutor
7
+ # Raised when a task is posted to an executor that is shutting down or has been shut down.
8
+ class RejectedExecutionError < RuntimeError
9
+ def initialize(msg = 'Executor has been shutdown and is no longer accepting tasks')
10
+ super
11
+ end
12
+ end
13
+
7
14
  DEFAULT_MAX_THREADS = 10
8
15
  RUNNING = :running
9
16
  SHUTTING_DOWN = :shutting_down
@@ -11,10 +18,12 @@ module Aws
11
18
 
12
19
  def initialize(options = {})
13
20
  @max_threads = options[:max_threads] || DEFAULT_MAX_THREADS
21
+ @max_queue = options[:max_queue] || 0
14
22
  @state = RUNNING
15
- @queue = Queue.new
23
+ @queue = @max_queue.zero? ? Queue.new : SizedQueue.new(@max_queue)
16
24
  @pool = []
17
25
  @mutex = Mutex.new
26
+ @fatal_error = nil
18
27
  end
19
28
 
20
29
  # Submits a task for execution.
@@ -23,12 +32,15 @@ module Aws
23
32
  # @return [Boolean] Returns true if the task was submitted successfully
24
33
  def post(*args, &block)
25
34
  @mutex.synchronize do
26
- raise 'Executor has been shutdown and is no longer accepting tasks' unless @state == RUNNING
35
+ raise RejectedExecutionError unless @state == RUNNING
27
36
 
28
- @queue << [args, block]
29
37
  ensure_worker_available
30
38
  end
39
+ # Outside the mutex so a caller blocked on a full queue can't hold up #shutdown or #kill.
40
+ @queue.push([args, block])
31
41
  true
42
+ rescue ClosedQueueError
43
+ raise RejectedExecutionError
32
44
  end
33
45
 
34
46
  # Immediately terminates all worker threads and clears pending tasks.
@@ -38,6 +50,7 @@ module Aws
38
50
  def kill
39
51
  @mutex.synchronize do
40
52
  @state = SHUTDOWN
53
+ @queue.close
41
54
  @pool.each(&:kill)
42
55
  @pool.clear
43
56
  @queue.clear
@@ -52,35 +65,58 @@ module Aws
52
65
  # If nil, waits indefinitely. If timeout expires, remaining threads are killed.
53
66
  # @return [Boolean] true when shutdown is complete
54
67
  def shutdown(timeout = nil)
68
+ return true unless begin_shutdown
69
+
70
+ deadline = timeout && (Time.now + timeout)
71
+ join_workers(deadline)
72
+ kill_remaining_workers if timeout
73
+
74
+ finalize_shutdown
75
+ raise @fatal_error if @fatal_error
76
+
77
+ true
78
+ end
79
+
80
+ private
81
+
82
+ def begin_shutdown
55
83
  @mutex.synchronize do
56
- return true if @state == SHUTDOWN
84
+ return false if @state == SHUTDOWN
57
85
 
58
86
  @state = SHUTTING_DOWN
59
- @pool.size.times { @queue << :shutdown }
87
+ # Close rather than push sentinels, which could block on a full queue.
88
+ @queue.close
60
89
  end
90
+ true
91
+ end
61
92
 
62
- if timeout
63
- deadline = Time.now + timeout
64
- @pool.each do |thread|
65
- remaining = deadline - Time.now
66
- break if remaining <= 0
93
+ def join_workers(deadline)
94
+ until (threads = @mutex.synchronize { @pool.select(&:alive?) }).empty?
95
+ threads.each do |thread|
96
+ remaining = deadline - Time.now if deadline
97
+ break if remaining && remaining <= 0
67
98
 
68
- thread.join([remaining, 0].max)
99
+ begin
100
+ thread.join(remaining && [remaining, 0].max)
101
+ rescue Exception # rubocop:disable Lint/RescueException
102
+ nil # recorded by #replace_worker and raised by #shutdown
103
+ end
69
104
  end
70
- @pool.select(&:alive?).each(&:kill)
71
- else
72
- @pool.each(&:join)
105
+ break if deadline && Time.now >= deadline
73
106
  end
107
+ end
108
+
109
+ def kill_remaining_workers
110
+ @mutex.synchronize { @pool.select(&:alive?).each(&:kill) }
111
+ end
74
112
 
113
+ def finalize_shutdown
75
114
  @mutex.synchronize do
76
115
  @pool.clear
77
116
  @state = SHUTDOWN
78
117
  end
79
- true
80
118
  end
81
119
 
82
- private
83
-
84
120
  def ensure_worker_available
85
121
  return unless @state == RUNNING
86
122
 
@@ -91,11 +127,21 @@ module Aws
91
127
  def spawn_worker
92
128
  Thread.new do
93
129
  while (job = @queue.shift)
94
- break if job == :shutdown
95
-
96
130
  args, block = job
97
131
  block.call(*args)
98
132
  end
133
+ rescue Exception => e # rubocop:disable Lint/RescueException
134
+ # Replace this worker so queued tasks still run.
135
+ replace_worker(e)
136
+ raise
137
+ end
138
+ end
139
+
140
+ def replace_worker(error)
141
+ @mutex.synchronize do
142
+ @fatal_error ||= error
143
+ @pool.delete(Thread.current)
144
+ @pool << spawn_worker unless @state == SHUTDOWN
99
145
  end
100
146
  end
101
147
  end
@@ -140,7 +140,7 @@ module Aws
140
140
  end
141
141
 
142
142
  def upload_with_executor(pending, completed, options)
143
- upload_attempts = 0
143
+ queued_parts = 0
144
144
  completion_queue = Queue.new
145
145
  abort_upload = false
146
146
  errors = []
@@ -149,25 +149,33 @@ module Aws
149
149
  while (part = pending.shift)
150
150
  break if abort_upload
151
151
 
152
- upload_attempts += 1
153
- @executor.post(part) do |p|
154
- Thread.current[:net_http_override_body_stream_chunk] = @http_chunk_size if @http_chunk_size
155
- update_progress(progress, p)
156
- resp = @client.upload_part(p)
157
- completed_part = { etag: resp.etag, part_number: p[:part_number] }
158
- apply_part_checksum(resp, completed_part)
159
- completed.push(completed_part)
152
+ begin
153
+ @executor.post(part) do |p|
154
+ Thread.current[:net_http_override_body_stream_chunk] = @http_chunk_size if @http_chunk_size
155
+ update_progress(progress, p)
156
+ resp = @client.upload_part(p)
157
+ completed_part = { etag: resp.etag, part_number: p[:part_number] }
158
+ apply_part_checksum(resp, completed_part)
159
+ completed.push(completed_part)
160
+ # Any error, or the upload completes without this part.
161
+ rescue Exception => e # rubocop:disable Lint/RescueException
162
+ abort_upload = true
163
+ errors << e
164
+ ensure
165
+ p[:body].close
166
+ Thread.current[:net_http_override_body_stream_chunk] = nil if @http_chunk_size
167
+ completion_queue << :done
168
+ end
169
+ queued_parts += 1
160
170
  rescue StandardError => e
171
+ # Rejected by the executor; abort rather than orphan the upload.
161
172
  abort_upload = true
162
173
  errors << e
163
- ensure
164
- p[:body].close
165
- Thread.current[:net_http_override_body_stream_chunk] = nil if @http_chunk_size
166
- completion_queue << :done
174
+ break
167
175
  end
168
176
  end
169
177
 
170
- upload_attempts.times { completion_queue.pop }
178
+ queued_parts.times { completion_queue.pop }
171
179
  errors
172
180
  end
173
181
 
@@ -113,18 +113,22 @@ module Aws
113
113
  def read_to_part_body(read_pipe)
114
114
  return if read_pipe.closed?
115
115
 
116
- temp_io = @tempfile ? Tempfile.new('aws-sdk-s3-upload_stream') : StringIO.new(String.new)
117
- temp_io.binmode
118
- bytes_copied = IO.copy_stream(read_pipe, temp_io, @part_size)
119
- temp_io.rewind
120
- if bytes_copied.zero?
121
- if temp_io.is_a?(Tempfile)
116
+ if @tempfile
117
+ temp_io = Tempfile.new('aws-sdk-s3-upload_stream')
118
+ temp_io.binmode
119
+ bytes_copied = IO.copy_stream(read_pipe, temp_io, @part_size)
120
+ temp_io.rewind
121
+ if bytes_copied.zero?
122
122
  temp_io.close
123
123
  temp_io.unlink
124
+ nil
125
+ else
126
+ temp_io
124
127
  end
125
- nil
126
128
  else
127
- temp_io
129
+ # A single sized read; copy_stream into a StringIO grows by doubling and fragments the heap.
130
+ data = read_pipe.read(@part_size)
131
+ data.nil? ? nil : StringIO.new(data)
128
132
  end
129
133
  end
130
134
 
@@ -139,20 +143,31 @@ module Aws
139
143
  end
140
144
  break unless part_body || current_part_num == 1
141
145
 
142
- queued_parts += 1
143
- @executor.post(part_body, current_part_num, options) do |body, num, opts|
144
- part = opts.merge(body: body, part_number: num)
145
- resp = @client.upload_part(part)
146
- completed_part = create_completed_part(resp, part)
147
- completed.push(completed_part)
146
+ begin
147
+ @executor.post(part_body, current_part_num, options) do |body, num, opts|
148
+ part = opts.merge(body: body, part_number: num)
149
+ resp = @client.upload_part(part)
150
+ completed_part = create_completed_part(resp, part)
151
+ completed.push(completed_part)
152
+ # Any error, or the upload completes without this part.
153
+ rescue Exception => e # rubocop:disable Lint/RescueException
154
+ mutex.synchronize do
155
+ errors.push(e)
156
+ read_pipe.close_read unless read_pipe.closed?
157
+ end
158
+ ensure
159
+ clear_body(body)
160
+ completion_queue << :done
161
+ end
162
+ queued_parts += 1
148
163
  rescue StandardError => e
164
+ # Rejected by the executor. Closing the pipe stops the producer so the upload can abort.
149
165
  mutex.synchronize do
150
166
  errors.push(e)
151
167
  read_pipe.close_read unless read_pipe.closed?
152
168
  end
153
- ensure
154
- clear_body(body)
155
- completion_queue << :done
169
+ clear_body(part_body)
170
+ break
156
171
  end
157
172
  end
158
173
  queued_parts.times { completion_queue.pop }
@@ -504,6 +504,9 @@ module Aws
504
504
  # @option options [Integer] :thread_count (10)
505
505
  # The number of parallel multipart uploads. Only used when no custom executor is provided (creates
506
506
  # {DefaultExecutor} with the given thread count). An additional thread is used internally for task coordination.
507
+ # This also bounds how many parts are buffered ahead of the upload, limiting memory usage to roughly
508
+ # `2 * :thread_count * :part_size`. When a custom `:executor` is provided, it is responsible for applying
509
+ # its own backpressure.
507
510
  #
508
511
  # @option options [Boolean] :tempfile (false)
509
512
  # Normally read data is stored in memory when building the parts in order to complete the underlying
@@ -524,8 +527,8 @@ module Aws
524
527
  # @see Client#upload_part
525
528
  def upload_stream(bucket:, key:, **options, &block)
526
529
  upload_opts = options.merge(bucket: bucket, key: key)
527
- thread_count = upload_opts.delete(:thread_count)
528
- executor = @executor || DefaultExecutor.new(max_threads: thread_count)
530
+ thread_count = upload_opts.delete(:thread_count) || DefaultExecutor::DEFAULT_MAX_THREADS
531
+ executor = @executor || DefaultExecutor.new(max_threads: thread_count, max_queue: thread_count)
529
532
  begin
530
533
  uploader = MultipartStreamUploader.new(
531
534
  client: @client,
data/lib/aws-sdk-s3.rb CHANGED
@@ -75,7 +75,7 @@ module Aws::S3
75
75
  autoload :ObjectVersion, 'aws-sdk-s3/object_version'
76
76
  autoload :EventStreams, 'aws-sdk-s3/event_streams'
77
77
 
78
- GEM_VERSION = '1.233.0'
78
+ GEM_VERSION = '1.233.1'
79
79
 
80
80
  end
81
81
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-sdk-s3
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.233.0
4
+ version: 1.233.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amazon Web Services