@@ -82,6 +82,9 @@ static struct multipart_upload *create_upload(struct flb_s3 *ctx,
8282 time_t file_first_log_time );
8383
8484static void remove_from_queue (struct upload_queue * entry );
85+ static void s3_chunk_retry_exhausted_cleanup (struct flb_s3 * ctx ,
86+ struct s3_file * chunk_file );
87+ static int s3_get_retry_exhausted_action (const char * value );
8588
8689static int blob_initialize_authorization_endpoint_upstream (struct flb_s3 * context );
8790
@@ -97,21 +100,21 @@ static struct flb_aws_header *get_content_encoding_header(int compression_type)
97100 .val = "gzip" ,
98101 .val_len = 4 ,
99102 };
100-
103+
101104 static struct flb_aws_header zstd_header = {
102105 .key = "Content-Encoding" ,
103106 .key_len = 16 ,
104107 .val = "zstd" ,
105108 .val_len = 4 ,
106109 };
107-
110+
108111 static struct flb_aws_header snappy_header = {
109112 .key = "Content-Encoding" ,
110113 .key_len = 16 ,
111114 .val = "snappy" ,
112115 .val_len = 6 ,
113116 };
114-
117+
115118 switch (compression_type ) {
116119 case FLB_AWS_COMPRESS_GZIP :
117120 return & gzip_header ;
@@ -196,7 +199,7 @@ int create_headers(struct flb_s3 *ctx, char *body_md5,
196199 if (ctx -> content_type != NULL ) {
197200 headers_len ++ ;
198201 }
199- if (ctx -> compression == FLB_AWS_COMPRESS_GZIP ||
202+ if (ctx -> compression == FLB_AWS_COMPRESS_GZIP ||
200203 ctx -> compression == FLB_AWS_COMPRESS_ZSTD ||
201204 ctx -> compression == FLB_AWS_COMPRESS_SNAPPY ) {
202205 headers_len ++ ;
@@ -228,7 +231,7 @@ int create_headers(struct flb_s3 *ctx, char *body_md5,
228231 s3_headers [n ].val_len = strlen (ctx -> content_type );
229232 n ++ ;
230233 }
231- if (ctx -> compression == FLB_AWS_COMPRESS_GZIP ||
234+ if (ctx -> compression == FLB_AWS_COMPRESS_GZIP ||
232235 ctx -> compression == FLB_AWS_COMPRESS_ZSTD ||
233236 ctx -> compression == FLB_AWS_COMPRESS_SNAPPY ) {
234237 encoding_header = get_content_encoding_header (ctx -> compression );
@@ -628,6 +631,7 @@ static int cb_s3_init(struct flb_output_instance *ins,
628631 struct flb_config * config , void * data )
629632{
630633 int ret ;
634+ int action ;
631635 flb_sds_t tmp_sds ;
632636 char * role_arn = NULL ;
633637 char * session_name ;
@@ -681,6 +685,15 @@ static int cb_s3_init(struct flb_output_instance *ins,
681685 return -1 ;
682686 }
683687
688+ action = s3_get_retry_exhausted_action (ctx -> retry_exhausted_action_str );
689+ if (action == -1 ) {
690+ flb_plg_error (ctx -> ins ,
691+ "invalid retry_exhausted_action value '%s'" ,
692+ ctx -> retry_exhausted_action_str ? ctx -> retry_exhausted_action_str : "(null)" );
693+ return -1 ;
694+ }
695+ ctx -> retry_exhausted_action = action ;
696+
684697 /* the check against -1 is works here because size_t is unsigned
685698 * and (int) -1 == unsigned max value
686699 * Fluent Bit uses -1 (which becomes max value) to indicate undefined
@@ -834,7 +847,7 @@ static int cb_s3_init(struct flb_output_instance *ins,
834847 "total_file_size is less than 10 MB, will use PutObject API" );
835848 ctx -> use_put_object = FLB_TRUE ;
836849 }
837-
850+
838851 if (ctx -> use_put_object == FLB_FALSE ) {
839852 /* upload_chunk_size */
840853 if (ctx -> upload_chunk_size <= 0 ) {
@@ -1446,6 +1459,9 @@ static int put_all_chunks(struct flb_s3 *ctx)
14461459 if (fs_stream == ctx -> stream_upload ) {
14471460 continue ;
14481461 }
1462+ if (fs_stream == ctx -> stream_quarantine ) {
1463+ continue ;
1464+ }
14491465 /* skip metadata stream */
14501466 if (fs_stream == ctx -> stream_metadata ) {
14511467 continue ;
@@ -1464,8 +1480,7 @@ static int put_all_chunks(struct flb_s3 *ctx)
14641480 flb_plg_warn (ctx -> ins ,
14651481 "Chunk for tag %s failed to send %d/%d times, will not retry" ,
14661482 (char * ) fsf -> meta_buf , chunk -> failures , ctx -> ins -> retry_limit );
1467- flb_free (chunk );
1468- flb_fstore_file_inactive (ctx -> fs , fsf );
1483+ s3_chunk_retry_exhausted_cleanup (ctx , chunk );
14691484 continue ;
14701485 }
14711486
@@ -1946,6 +1961,72 @@ static int buffer_chunk(void *out_context, struct s3_file *upload_file,
19461961 return 0 ;
19471962}
19481963
1964+ /*
1965+ * Terminal retry exhaustion must permanently remove local buffer files.
1966+ * Unlike inactive state (recoverable/restart-resumable), terminal cleanup
1967+ * must release store_dir_limit_size accounting and delete on-disk state.
1968+ */
1969+ static void s3_chunk_retry_exhausted_cleanup (struct flb_s3 * ctx ,
1970+ struct s3_file * chunk_file )
1971+ {
1972+ size_t reclaim_size ;
1973+ int ret ;
1974+
1975+ if (chunk_file == NULL ) {
1976+ return ;
1977+ }
1978+
1979+ if (ctx -> retry_exhausted_action == S3_RETRY_EXHAUSTED_QUARANTINE ) {
1980+ reclaim_size = chunk_file -> size ;
1981+
1982+ if (ctx -> quarantine_dir_limit_size > 0 &&
1983+ ctx -> quarantine_buffer_size + reclaim_size > ctx -> quarantine_dir_limit_size ) {
1984+ flb_plg_warn (ctx -> ins ,
1985+ "quarantine limit reached, deleting retry-exhausted chunk" );
1986+ s3_store_file_delete (ctx , chunk_file );
1987+ return ;
1988+ }
1989+
1990+ ret = s3_store_file_quarantine (ctx , chunk_file );
1991+ if (ret < 0 ) {
1992+ flb_plg_error (ctx -> ins ,
1993+ "could not quarantine, deleting retry-exhausted chunk" );
1994+ s3_store_file_delete (ctx , chunk_file );
1995+ return ;
1996+ }
1997+
1998+ if (ctx -> current_buffer_size >= reclaim_size ) {
1999+ ctx -> current_buffer_size -= reclaim_size ;
2000+ }
2001+ else {
2002+ ctx -> current_buffer_size = 0 ;
2003+ }
2004+ ctx -> quarantine_buffer_size += reclaim_size ;
2005+
2006+ flb_plg_warn (ctx -> ins ,
2007+ "retry-exhausted chunk moved to quarantine" );
2008+ return ;
2009+ }
2010+
2011+ s3_store_file_delete (ctx , chunk_file );
2012+ }
2013+
2014+ static int s3_get_retry_exhausted_action (const char * value )
2015+ {
2016+ if (value == NULL ) {
2017+ return S3_RETRY_EXHAUSTED_QUARANTINE ;
2018+ }
2019+
2020+ if (strcasecmp (value , "delete" ) == 0 ) {
2021+ return S3_RETRY_EXHAUSTED_DELETE ;
2022+ }
2023+ if (strcasecmp (value , "quarantine" ) == 0 ) {
2024+ return S3_RETRY_EXHAUSTED_QUARANTINE ;
2025+ }
2026+
2027+ return -1 ;
2028+ }
2029+
19492030/* Uploads all chunk files in queue synchronously */
19502031static void s3_upload_queue (struct flb_config * config , void * out_context )
19512032{
@@ -1998,7 +2079,7 @@ static void s3_upload_queue(struct flb_config *config, void *out_context)
19982079 if (upload_contents -> retry_counter > ctx -> ins -> retry_limit ) {
19992080 flb_plg_warn (ctx -> ins , "Chunk file failed to send %d times, will not "
20002081 "retry" , upload_contents -> retry_counter );
2001- s3_store_file_inactive (ctx , upload_contents -> upload_file );
2082+ s3_chunk_retry_exhausted_cleanup (ctx , upload_contents -> upload_file );
20022083 if (upload_contents -> m_upload_file ) {
20032084 mk_list_del (& upload_contents -> m_upload_file -> _head );
20042085 }
@@ -3403,8 +3484,7 @@ static void cb_s3_upload(struct flb_config *config, void *data)
34033484 flb_plg_warn (ctx -> ins ,
34043485 "Chunk for tag %s failed to send %d/%d times, will not retry" ,
34053486 (char * ) fsf -> meta_buf , chunk -> failures , ctx -> ins -> retry_limit );
3406- flb_free (chunk );
3407- flb_fstore_file_inactive (ctx -> fs , fsf );
3487+ s3_chunk_retry_exhausted_cleanup (ctx , chunk );
34083488 continue ;
34093489 }
34103490 }
@@ -3937,7 +4017,7 @@ static void cb_s3_flush(struct flb_event_chunk *event_chunk,
39374017 if (upload_file != NULL && upload_file -> failures > ctx -> ins -> retry_limit ) {
39384018 flb_plg_warn (ctx -> ins , "File with tag %s failed to send %d/%d times, will not retry" ,
39394019 event_chunk -> tag , upload_file -> failures , ctx -> ins -> retry_limit );
3940- s3_store_file_inactive (ctx , upload_file );
4020+ s3_chunk_retry_exhausted_cleanup (ctx , upload_file );
39414021 upload_file = NULL ;
39424022 }
39434023
@@ -4177,6 +4257,13 @@ static struct flb_config_map config_map[] = {
41774257 "the `store_dir` to limit disk usage. If the limit is reached, "
41784258 "data will be discarded. Default is 0 which means unlimited."
41794259 },
4260+ {
4261+ FLB_CONFIG_MAP_SIZE , "quarantine_dir_limit_size" , "0" ,
4262+ 0 , FLB_TRUE , offsetof(struct flb_s3 , quarantine_dir_limit_size ),
4263+ "Limit size for retry-exhausted quarantined chunks. Applies when "
4264+ "retry_exhausted_action is set to 'quarantine'. If limit is reached, "
4265+ "retry-exhausted chunks are deleted. Default is 0 (unlimited)."
4266+ },
41804267
41814268 {
41824269 FLB_CONFIG_MAP_STR , "s3_key_format" , "/fluent-bit-logs/$TAG/%Y/%m/%d/%H/%M/%S" ,
@@ -4229,6 +4316,13 @@ static struct flb_config_map config_map[] = {
42294316 "received chunk to be swapped with a later chunk, resulting in data shuffling. "
42304317 "This feature prevents this shuffling by using a queue logic for uploads."
42314318 },
4319+ {
4320+ FLB_CONFIG_MAP_STR , "retry_exhausted_action" , "quarantine" ,
4321+ 0 , FLB_TRUE , offsetof(struct flb_s3 , retry_exhausted_action_str ),
4322+ "Action for chunks that exceeded retry_limit. Supported values are "
4323+ "'delete' (remove permanently) and 'quarantine' (move to quarantine and "
4324+ "remove from active buffer accounting)."
4325+ },
42324326
42334327 {
42344328 FLB_CONFIG_MAP_STR , "log_key" , NULL ,
0 commit comments