logstash-integration-aws 7.0.0 → 7.0.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: 4cea87525319253265e10b11632067a0470dd461bcd6febad35e13bb94d5e2b2
4
- data.tar.gz: ae9b187795bc416f1c1e7e16b4207b8fc476b54c4cbc55325104f91930a65ae1
3
+ metadata.gz: 17a23ba90d8888e550c2116d81a5dde97b5e57f762c02ceea44a2afecb3e5579
4
+ data.tar.gz: 41d6e0d5e4e5d9bed6170baa2b44fdfc4bce7d89488e0f15bd80ff9e7a44a3c0
5
5
  SHA512:
6
- metadata.gz: e62194ffbe42ac97834a189eb9821feed9eeebd28bb72ffed26de4b087d76b75ca30006c630d2d30f1892f7dc41825e176bf25fd97487e3554aec5fc7e847b36
7
- data.tar.gz: 30e9270e7933fd448ff1e9ae41f768edbe6fd956719c79e9f828743e583d06ddef1fa2ab55462df6c7334990b01e14fb766290c1a8f9e405cf1e8673a99f4d83
6
+ metadata.gz: 14cef0ec2b9d04466fcf9ee6f612cd2c8e1055bc5853b147c9086c4914d52c695aab3ab270ef81fe9ae3c9add45391a380228d77befdfe54910decfc27ad60fd
7
+ data.tar.gz: 5da492ac21e100028d58a6549efae8d110e764ff28a79d21f835946c41afba88e11d240fedde9ac2842357b87e9610f31e098c8ffbd7946ffc36ca5555cd170a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 7.0.1
2
+ - resolves two closely-related race conditions in the S3 Output plugin's handling of stale temporary files that could cause plugin crashes or data-loss [#19](https://github.com/logstash-plugins/logstash-integration-aws/pull/19)
3
+ - prevents a `No such file or directory` crash that could occur when a temporary file is accessed after it has been detected as stale (empty+old) and deleted.
4
+ - prevents a possible deletion of a non-empty temporary file that could occur if bytes were written to it _after_ it was detected as stale (empty+old) and _before_ the deletion completed.
5
+
1
6
  ## 7.0.0
2
7
  - bump integration to upper bound of all underlying plugins versions (biggest is sqs output 6.x)
3
8
  - this is necessary to facilitate versioning continuity between older standalone plugins and plugins within the integration
@@ -1,4 +1,4 @@
1
- // :integration: aws
1
+ :integration: aws
2
2
  :plugin: cloudfront
3
3
  :type: codec
4
4
 
@@ -17,7 +17,7 @@ END - GENERATED VARIABLES, DO NOT EDIT!
17
17
 
18
18
  === Cloudfront codec plugin
19
19
 
20
- // include::{include_path}/plugin_header-integration.asciidoc[]
20
+ include::{include_path}/plugin_header-integration.asciidoc[]
21
21
 
22
22
  ==== Description
23
23
 
@@ -1,4 +1,4 @@
1
- // :integration: aws
1
+ :integration: aws
2
2
  :plugin: cloudtrail
3
3
  :type: codec
4
4
 
@@ -17,7 +17,7 @@ END - GENERATED VARIABLES, DO NOT EDIT!
17
17
 
18
18
  === Cloudtrail codec plugin
19
19
 
20
- // include::{include_path}/plugin_header-integration.asciidoc[]
20
+ include::{include_path}/plugin_header-integration.asciidoc[]
21
21
 
22
22
  ==== Description
23
23
 
data/docs/index.asciidoc CHANGED
@@ -17,13 +17,12 @@ END - GENERATED VARIABLES, DO NOT EDIT!
17
17
 
18
18
  === AWS Integration Plugin
19
19
 
20
- // include::{include_path}/plugin_header.asciidoc[]
20
+ include::{include_path}/plugin_header.asciidoc[]
21
21
 
22
22
  ==== Description
23
23
 
24
24
  The AWS Integration Plugin provides integrated plugins for working with Amazon Web Services:
25
25
 
26
- ////
27
26
  - {logstash-ref}/plugins-codecs-cloudfront.html[Cloudfront Codec Plugin]
28
27
  - {logstash-ref}/plugins-codecs-cloudtrail.html[Cloudtrail Codec Plugin]
29
28
  - {logstash-ref}/plugins-inputs-cloudwatch.html[Cloudwatch Input Plugin]
@@ -33,6 +32,5 @@ The AWS Integration Plugin provides integrated plugins for working with Amazon W
33
32
  - {logstash-ref}/plugins-outputs-s3.html[S3 Output Plugin]
34
33
  - {logstash-ref}/plugins-outputs-sns.html[Sns Output Plugin]
35
34
  - {logstash-ref}/plugins-outputs-sqs.html[Sqs Output Plugin]
36
- ////
37
35
 
38
36
  :no_codec!:
@@ -1,4 +1,4 @@
1
- // :integration: aws
1
+ :integration: aws
2
2
  :plugin: cloudwatch
3
3
  :type: input
4
4
  :default_codec: plain
@@ -18,7 +18,7 @@ END - GENERATED VARIABLES, DO NOT EDIT!
18
18
 
19
19
  === Cloudwatch input plugin
20
20
 
21
- // include::{include_path}/plugin_header-integration.asciidoc[]
21
+ include::{include_path}/plugin_header-integration.asciidoc[]
22
22
 
23
23
  ==== Description
24
24
 
@@ -1,4 +1,4 @@
1
- // :integration: aws
1
+ :integration: aws
2
2
  :plugin: s3
3
3
  :type: input
4
4
  :default_codec: plain
@@ -18,7 +18,7 @@ END - GENERATED VARIABLES, DO NOT EDIT!
18
18
 
19
19
  === S3 input plugin
20
20
 
21
- // include::{include_path}/plugin_header-integration.asciidoc[]
21
+ include::{include_path}/plugin_header-integration.asciidoc[]
22
22
 
23
23
  ==== Description
24
24
 
@@ -1,4 +1,4 @@
1
- // :integration: aws
1
+ :integration: aws
2
2
  :plugin: sqs
3
3
  :type: input
4
4
  :default_codec: json
@@ -18,7 +18,7 @@ END - GENERATED VARIABLES, DO NOT EDIT!
18
18
 
19
19
  === Sqs input plugin
20
20
 
21
- // include::{include_path}/plugin_header-integration.asciidoc[]
21
+ include::{include_path}/plugin_header-integration.asciidoc[]
22
22
 
23
23
  ==== Description
24
24
 
@@ -1,4 +1,4 @@
1
- // :integration: aws
1
+ :integration: aws
2
2
  :plugin: cloudwatch
3
3
  :type: output
4
4
  :default_codec: plain
@@ -18,7 +18,7 @@ END - GENERATED VARIABLES, DO NOT EDIT!
18
18
 
19
19
  === Cloudwatch output plugin
20
20
 
21
- // include::{include_path}/plugin_header-integration.asciidoc[]
21
+ include::{include_path}/plugin_header-integration.asciidoc[]
22
22
 
23
23
  ==== Description
24
24
 
@@ -1,4 +1,4 @@
1
- // :integration: aws
1
+ :integration: aws
2
2
  :plugin: s3
3
3
  :type: output
4
4
  :default_codec: line
@@ -18,7 +18,7 @@ END - GENERATED VARIABLES, DO NOT EDIT!
18
18
 
19
19
  === S3 output plugin
20
20
 
21
- // include::{include_path}/plugin_header-integration.asciidoc[]
21
+ include::{include_path}/plugin_header-integration.asciidoc[]
22
22
 
23
23
  ==== Description
24
24
 
@@ -1,4 +1,4 @@
1
- // :integration: aws
1
+ :integration: aws
2
2
  :plugin: sns
3
3
  :type: output
4
4
  :default_codec: plain
@@ -18,7 +18,7 @@ END - GENERATED VARIABLES, DO NOT EDIT!
18
18
 
19
19
  === Sns output plugin
20
20
 
21
- // include::{include_path}/plugin_header-integration.asciidoc[]
21
+ include::{include_path}/plugin_header-integration.asciidoc[]
22
22
 
23
23
  ==== Description
24
24
 
@@ -1,4 +1,4 @@
1
- // :integration: aws
1
+ :integration: aws
2
2
  :plugin: sqs
3
3
  :type: output
4
4
  :default_codec: json
@@ -18,7 +18,7 @@ END - GENERATED VARIABLES, DO NOT EDIT!
18
18
 
19
19
  === Sqs output plugin
20
20
 
21
- // include::{include_path}/plugin_header-integration.asciidoc[]
21
+ include::{include_path}/plugin_header-integration.asciidoc[]
22
22
 
23
23
  ==== Description
24
24
 
@@ -17,8 +17,9 @@ module LogStash
17
17
  class PrefixedValue
18
18
  def initialize(file_factory, stale_time)
19
19
  @file_factory = file_factory
20
- @lock = Mutex.new
20
+ @lock = Monitor.new # reentrant Mutex
21
21
  @stale_time = stale_time
22
+ @is_deleted = false
22
23
  end
23
24
 
24
25
  def with_lock
@@ -36,7 +37,14 @@ module LogStash
36
37
  end
37
38
 
38
39
  def delete!
39
- with_lock{ |factory| factory.current.delete! }
40
+ with_lock do |factory|
41
+ factory.current.delete!
42
+ @is_deleted = true
43
+ end
44
+ end
45
+
46
+ def deleted?
47
+ with_lock { |_| @is_deleted }
40
48
  end
41
49
  end
42
50
 
@@ -72,19 +80,70 @@ module LogStash
72
80
  @prefixed_factories.keySet
73
81
  end
74
82
 
83
+ ##
84
+ # Yields the current file of each non-deleted file factory while the current thread has exclusive access to it.
85
+ # @yieldparam file [TemporaryFile]
86
+ # @return [void]
75
87
  def each_files
76
- @prefixed_factories.elements.each do |prefixed_file|
77
- prefixed_file.with_lock { |factory| yield factory.current }
88
+ each_factory(keys) do |factory|
89
+ yield factory.current
78
90
  end
91
+ nil # void return avoid leaking unsynchronized access
79
92
  end
80
93
 
81
- # Return the file factory
94
+ ##
95
+ # Yields the file factory while the current thread has exclusive access to it, creating a new
96
+ # one if one does not exist or if the current one is being reaped by the stale watcher.
97
+ # @param prefix_key [String]: the prefix key
98
+ # @yieldparam factory [TemporaryFileFactory]: a temporary file factory that this thread has exclusive access to
99
+ # @return [void]
82
100
  def get_factory(prefix_key)
83
- @prefixed_factories.computeIfAbsent(prefix_key, @factory_initializer).with_lock { |factory| yield factory }
101
+ # fast-path: if factory exists and is not deleted, yield it with exclusive access and return
102
+ prefix_val = @prefixed_factories.get(prefix_key)
103
+ prefix_val&.with_lock do |factory|
104
+ # intentional local-jump to ensure deletion detection
105
+ # is done inside the exclusive access.
106
+ unless prefix_val.deleted?
107
+ yield(factory)
108
+ return nil # void return avoid leaking unsynchronized access
109
+ end
110
+ end
111
+
112
+ # slow-path:
113
+ # the ConcurrentHashMap#get operation is lock-free, but may have returned an entry that was being deleted by
114
+ # another thread (such as via stale detection). If we failed to retrieve a value, or retrieved one that had
115
+ # been marked deleted, use the atomic ConcurrentHashMap#compute to retrieve a non-deleted entry.
116
+ prefix_val = @prefixed_factories.compute(prefix_key) do |_, existing|
117
+ existing && !existing.deleted? ? existing : @factory_initializer.apply(prefix_key)
118
+ end
119
+ prefix_val.with_lock { |factory| yield factory }
120
+ nil # void return avoid leaking unsynchronized access
121
+ end
122
+
123
+ ##
124
+ # Yields each non-deleted file factory while the current thread has exclusive access to it.
125
+ # @param prefixes [Array<String>]: the prefix keys
126
+ # @yieldparam factory [TemporaryFileFactory]
127
+ # @return [void]
128
+ def each_factory(prefixes)
129
+ prefixes.each do |prefix_key|
130
+ prefix_val = @prefixed_factories.get(prefix_key)
131
+ prefix_val&.with_lock do |factory|
132
+ yield factory unless prefix_val.deleted?
133
+ end
134
+ end
135
+ nil # void return avoid leaking unsynchronized access
84
136
  end
85
137
 
138
+ ##
139
+ # Ensures that a non-deleted factory exists for the provided prefix and yields its current file
140
+ # while the current thread has exclusive access to it.
141
+ # @param prefix_key [String]
142
+ # @yieldparam file [TemporaryFile]
143
+ # @return [void]
86
144
  def get_file(prefix_key)
87
145
  get_factory(prefix_key) { |factory| yield factory.current }
146
+ nil # void return avoid leaking unsynchronized access
88
147
  end
89
148
 
90
149
  def shutdown
@@ -95,10 +154,21 @@ module LogStash
95
154
  @prefixed_factories.size
96
155
  end
97
156
 
98
- def remove_stale(k, v)
99
- if v.stale?
100
- @prefixed_factories.remove(k, v)
101
- v.delete!
157
+ def remove_if_stale(prefix_key)
158
+ # we use the ATOMIC `ConcurrentHashMap#computeIfPresent` to atomically
159
+ # detect the staleness, mark a stale prefixed factory as deleted, and delete from the map.
160
+ @prefixed_factories.computeIfPresent(prefix_key) do |_, prefixed_factory|
161
+ # once we have retrieved an instance, we acquire exclusive access to it
162
+ # for stale detection, marking it as deleted before releasing the lock
163
+ # and causing it to become deleted from the map.
164
+ prefixed_factory.with_lock do |_|
165
+ if prefixed_factory.stale?
166
+ prefixed_factory.delete! # mark deleted to prevent reuse
167
+ nil # cause deletion
168
+ else
169
+ prefixed_factory # keep existing
170
+ end
171
+ end
102
172
  end
103
173
  end
104
174
 
@@ -106,7 +176,9 @@ module LogStash
106
176
  @stale_sweeper = Concurrent::TimerTask.new(:execution_interval => @sweeper_interval) do
107
177
  LogStash::Util.set_thread_name("S3, Stale factory sweeper")
108
178
 
109
- @prefixed_factories.forEach{|k,v| remove_stale(k,v)}
179
+ @prefixed_factories.keys.each do |prefix|
180
+ remove_if_stale(prefix)
181
+ end
110
182
  end
111
183
 
112
184
  @stale_sweeper.execute
@@ -258,6 +258,8 @@ class LogStash::Outputs::S3 < LogStash::Outputs::Base
258
258
 
259
259
  @logger.debug("Uploading current workspace")
260
260
 
261
+ @file_repository.shutdown # stop stale sweeps
262
+
261
263
  # The plugin has stopped receiving new events, but we still have
262
264
  # data on disk, lets make sure it get to S3.
263
265
  # If Logstash get interrupted, the `restore_from_crash` (when set to true) method will pickup
@@ -267,8 +269,6 @@ class LogStash::Outputs::S3 < LogStash::Outputs::Base
267
269
  upload_file(file)
268
270
  end
269
271
 
270
- @file_repository.shutdown
271
-
272
272
  @uploader.stop # wait until all the current upload are complete
273
273
  @crash_uploader.stop if @restore # we might have still work to do for recovery so wait until we are done
274
274
  end
@@ -336,22 +336,21 @@ class LogStash::Outputs::S3 < LogStash::Outputs::Base
336
336
  end
337
337
 
338
338
  def rotate_if_needed(prefixes)
339
- prefixes.each do |prefix|
340
- # Each file access is thread safe,
341
- # until the rotation is done then only
342
- # one thread has access to the resource.
343
- @file_repository.get_factory(prefix) do |factory|
344
- temp_file = factory.current
345
-
346
- if @rotation.rotate?(temp_file)
347
- @logger.debug("Rotate file",
348
- :strategy => @rotation.class.name,
349
- :key => temp_file.key,
350
- :path => temp_file.path)
351
-
352
- upload_file(temp_file)
353
- factory.rotate!
354
- end
339
+
340
+ # Each file access is thread safe,
341
+ # until the rotation is done then only
342
+ # one thread has access to the resource.
343
+ @file_repository.each_factory(prefixes) do |factory|
344
+ temp_file = factory.current
345
+
346
+ if @rotation.rotate?(temp_file)
347
+ @logger.debug("Rotate file",
348
+ :strategy => @rotation.class.name,
349
+ :key => temp_file.key,
350
+ :path => temp_file.path)
351
+
352
+ upload_file(temp_file)
353
+ factory.rotate!
355
354
  end
356
355
  end
357
356
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "logstash-integration-aws"
3
- s.version = "7.0.0"
3
+ s.version = "7.0.1"
4
4
  s.licenses = ["Apache-2.0"]
5
5
  s.summary = "Collection of Logstash plugins that integrate with AWS"
6
6
  s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-integration-aws
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0
4
+ version: 7.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-25 00:00:00.000000000 Z
11
+ date: 2022-12-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement