GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/upload_s3.cc
Date: 2026-09-27 02:40:09
Exec Total Coverage
Lines: 239 350 68.3%
Branches: 209 524 39.9%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 */
4
5 #include "upload_s3.h"
6
7 #include <errno.h>
8 #include <fcntl.h>
9 #include <inttypes.h>
10 #include <unistd.h>
11
12 #include <set>
13 #include <string>
14 #include <vector>
15
16 #include "compression/compression.h"
17 #include "network/s3fanout.h"
18 #include "options.h"
19 #include "util/exception.h"
20 #include "util/logging.h"
21 #include "util/mutex.h"
22 #include "util/posix.h"
23 #include "util/string.h"
24
25 namespace upload {
26
27 /*
28 * Allowed values of x-amz-acl according to S3 API
29 */
30 static const char *x_amz_acl_allowed_values_[8] = {"private",
31 "public-read",
32 "public-write",
33 "authenticated-read",
34 "aws-exec-read",
35 "bucket-owner-read",
36 "bucket-owner-full-control",
37 ""};
38
39 2560 void S3Uploader::RequestCtrl::WaitFor() {
40 char c;
41
1/2
✓ Branch 1 taken 2560 times.
✗ Branch 2 not taken.
2560 ReadPipe(pipe_wait[0], &c, 1);
42
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2560 times.
2560 assert(c == 'c');
43
1/2
✓ Branch 1 taken 2560 times.
✗ Branch 2 not taken.
2560 ClosePipe(pipe_wait);
44 2560 }
45
46
47 65 S3Uploader::S3Uploader(const SpoolerDefinition &spooler_definition)
48 : AbstractUploader(spooler_definition)
49 65 , dns_buckets_(true)
50 65 , num_parallel_uploads_(kDefaultNumParallelUploads)
51 65 , num_retries_(kDefaultNumRetries)
52 65 , timeout_sec_(kDefaultTimeoutSec)
53 65 , authz_method_(s3fanout::kAuthzAwsV2)
54 65 , peek_before_put_(true)
55 65 , use_https_(false)
56 65 , batch_delete_enabled_(true)
57 65 , batch_delete_size_(kDefaultBatchDeleteSize)
58
1/2
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
65 , proxy_("")
59
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 , temporary_path_(spooler_definition.temporary_path)
60
1/2
✓ Branch 12 taken 65 times.
✗ Branch 13 not taken.
130 , x_amz_acl_("public-read") {
61
2/4
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 65 times.
✗ Branch 4 not taken.
65 assert(spooler_definition.IsValid()
62 && spooler_definition.driver_type == SpoolerDefinition::S3);
63
64 65 atomic_init32(&io_errors_);
65 65 const int mutex_ret = pthread_mutex_init(&delete_batch_mutex_, NULL);
66
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 assert(mutex_ret == 0);
67
68
2/4
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 65 times.
65 if (!ParseSpoolerDefinition(spooler_definition)) {
69 ✗ PANIC(kLogStderr, "Error in parsing the spooler definition");
70 }
71
72 // Disable batch delete for Azure (not supported)
73
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 if (authz_method_ == s3fanout::kAuthzAzure)
74 ✗ batch_delete_enabled_ = false;
75
76
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 s3fanout::S3FanoutManager::S3Config s3config;
77
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 s3config.access_key = access_key_;
78
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 s3config.secret_key = secret_key_;
79
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 s3config.hostname_port = host_name_port_;
80 65 s3config.authz_method = authz_method_;
81
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 s3config.region = region_;
82
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 s3config.flavor = flavor_;
83
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 s3config.bucket = bucket_;
84 65 s3config.dns_buckets = dns_buckets_;
85 65 s3config.pool_max_handles = num_parallel_uploads_;
86 65 s3config.opt_timeout_sec = timeout_sec_;
87 65 s3config.opt_max_retries = num_retries_;
88 65 s3config.opt_backoff_init_ms = kDefaultBackoffInitMs;
89 65 s3config.opt_backoff_max_ms = kDefaultBackoffMaxMs;
90
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 s3config.x_amz_acl = x_amz_acl_;
91
92
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 if (use_https_) {
93 ✗ s3config.protocol = "https";
94 } else {
95
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 s3config.protocol = "http";
96 }
97
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 s3config.proxy = proxy_;
98
99 130 s3fanout_mgr_ = std::unique_ptr<s3fanout::S3FanoutManager>(
100
2/4
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 65 times.
✗ Branch 5 not taken.
130 new s3fanout::S3FanoutManager(s3config));
101
1/2
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
65 s3fanout_mgr_->Spawn();
102
103 65 const int retval = pthread_create(&thread_collect_results_, NULL,
104 MainCollectResults, this);
105
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 assert(retval == 0);
106 65 }
107
108
109 390 S3Uploader::~S3Uploader() {
110 // Signal termination to our own worker thread
111 130 s3fanout_mgr_->PushCompletedJob(NULL);
112 130 pthread_join(thread_collect_results_, NULL);
113 130 pthread_mutex_destroy(&delete_batch_mutex_);
114 260 }
115
116
117 65 bool S3Uploader::ParseSpoolerDefinition(
118 const SpoolerDefinition &spooler_definition) {
119 const std::vector<std::string> config = SplitString(
120
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 spooler_definition.spooler_configuration, '@');
121
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 65 times.
65 if (config.size() != 2) {
122 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
123 "Failed to parse spooler configuration string '%s'.\n"
124 "Provide: <repo_alias>@/path/to/s3.conf",
125 spooler_definition.spooler_configuration.c_str());
126 ✗ return false;
127 }
128
1/2
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
65 repository_alias_ = config[0];
129 65 const std::string &config_path = config[1];
130
131
2/4
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 65 times.
65 if (!FileExists(config_path)) {
132 ✗ LogCvmfs(kLogUploadS3, kLogStderr, "Cannot find S3 config file at '%s'",
133 config_path.c_str());
134 ✗ return false;
135 }
136
137 // Parse S3 configuration
138 BashOptionsManager options_manager = BashOptionsManager(
139
4/8
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 65 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 65 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 65 times.
✗ Branch 11 not taken.
65 new DefaultOptionsTemplateManager(repository_alias_));
140
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 options_manager.ParsePath(config_path, false);
141 65 std::string parameter;
142
143
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (!options_manager.GetValue("CVMFS_S3_HOST", &host_name_)) {
144 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
145 "Failed to parse CVMFS_S3_HOST from '%s'", config_path.c_str());
146 ✗ return false;
147 }
148
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (!options_manager.GetValue("CVMFS_S3_ACCESS_KEY", &access_key_)) {
149 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
150 "Failed to parse CVMFS_S3_ACCESS_KEY from '%s'.",
151 config_path.c_str());
152 ✗ return false;
153 }
154
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (!options_manager.GetValue("CVMFS_S3_SECRET_KEY", &secret_key_)) {
155 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
156 "Failed to parse CVMFS_S3_SECRET_KEY from '%s'.",
157 config_path.c_str());
158 ✗ return false;
159 }
160
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (!options_manager.GetValue("CVMFS_S3_BUCKET", &bucket_)) {
161 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
162 "Failed to parse CVMFS_S3_BUCKET from '%s'.", config_path.c_str());
163 ✗ return false;
164 }
165
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✓ Branch 9 taken 65 times.
✗ Branch 10 not taken.
65 if (options_manager.GetValue("CVMFS_S3_DNS_BUCKETS", &parameter)) {
166
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 if (parameter == "false") {
167 65 dns_buckets_ = false;
168 }
169 }
170
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✓ Branch 9 taken 65 times.
✗ Branch 10 not taken.
65 if (options_manager.GetValue("CVMFS_S3_MAX_NUMBER_OF_PARALLEL_CONNECTIONS",
171 &parameter)) {
172
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 num_parallel_uploads_ = String2Uint64(parameter);
173 }
174
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (options_manager.GetValue("CVMFS_S3_MAX_RETRIES", &parameter)) {
175 ✗ num_retries_ = String2Uint64(parameter);
176 }
177
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (options_manager.GetValue("CVMFS_S3_TIMEOUT", &parameter)) {
178 ✗ timeout_sec_ = String2Uint64(parameter);
179 }
180
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (options_manager.GetValue("CVMFS_S3_REGION", &region_)) {
181 ✗ authz_method_ = s3fanout::kAuthzAwsV4;
182 }
183
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (options_manager.GetValue("CVMFS_S3_FLAVOR", &flavor_)) {
184 ✗ if (flavor_ == "azure") {
185 ✗ authz_method_ = s3fanout::kAuthzAzure;
186 ✗ } else if (flavor_ == "awsv2") {
187 ✗ authz_method_ = s3fanout::kAuthzAwsV2;
188 ✗ } else if (flavor_ == "awsv4") {
189 ✗ authz_method_ = s3fanout::kAuthzAwsV4;
190 } else {
191 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
192 "Failed to parse CVMFS_S3_FLAVOR from '%s', "
193 "valid options are azure, awsv2 or awsv4",
194 config_path.c_str());
195 ✗ return false;
196 }
197 }
198
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (options_manager.GetValue("CVMFS_S3_PEEK_BEFORE_PUT", &parameter)) {
199 ✗ peek_before_put_ = options_manager.IsOn(parameter);
200 }
201
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (options_manager.GetValue("CVMFS_S3_X_AMZ_ACL", &parameter)) {
202 ✗ bool isAllowed = false;
203 ✗ size_t const len = sizeof(x_amz_acl_allowed_values_)
204 / sizeof(x_amz_acl_allowed_values_[0]);
205 ✗ for (size_t i = 0; i < len; i++) {
206 ✗ if (x_amz_acl_allowed_values_[i] == parameter) {
207 ✗ isAllowed = true;
208 ✗ break;
209 }
210 }
211 ✗ if (!isAllowed) {
212 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
213 "%s is not an allowed value for CVMFS_S3_X_AMZ_ACL",
214 parameter.c_str());
215 ✗ return false;
216 }
217 ✗ x_amz_acl_ = parameter;
218 }
219
220
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (options_manager.GetValue("CVMFS_S3_BATCH_DELETE", &parameter)) {
221 ✗ batch_delete_enabled_ = options_manager.IsOn(parameter);
222 }
223
224
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (options_manager.GetValue("CVMFS_S3_BATCH_DELETE_SIZE", &parameter)) {
225 ✗ const unsigned requested = String2Uint64(parameter);
226 ✗ if (requested == 0) {
227 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
228 "CVMFS_S3_BATCH_DELETE_SIZE must be > 0, using default %u",
229 kDefaultBatchDeleteSize);
230 ✗ batch_delete_size_ = kDefaultBatchDeleteSize;
231 ✗ } else if (requested > kMaxBatchDeleteSize) {
232 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
233 "Warning: CVMFS_S3_BATCH_DELETE_SIZE=%u exceeds the S3 "
234 "multi-object DELETE limit of %u, clamping to %u",
235 requested, kMaxBatchDeleteSize, kMaxBatchDeleteSize);
236 ✗ batch_delete_size_ = kMaxBatchDeleteSize;
237 } else {
238 ✗ batch_delete_size_ = requested;
239 }
240 }
241
242
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (options_manager.GetValue("CVMFS_S3_USE_HTTPS", &parameter)) {
243 ✗ use_https_ = options_manager.IsOn(parameter);
244 }
245
246
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✓ Branch 9 taken 65 times.
✗ Branch 10 not taken.
65 if (options_manager.GetValue("CVMFS_S3_PORT", &parameter)) {
247
2/4
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 65 times.
✗ Branch 5 not taken.
65 host_name_port_ = host_name_ + ":" + parameter;
248 } else {
249 ✗ host_name_port_ = host_name_;
250 }
251
252
3/6
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 65 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 65 times.
65 if (options_manager.IsDefined("CVMFS_S3_PROXY")) {
253 ✗ options_manager.GetValue("CVMFS_S3_PROXY", &proxy_);
254 }
255
256 65 return true;
257 65 }
258
259
260 173 bool S3Uploader::WillHandle(const SpoolerDefinition &spooler_definition) {
261 173 return spooler_definition.driver_type == SpoolerDefinition::S3;
262 }
263
264
265 ✗ bool S3Uploader::Create() {
266 ✗ if (!dns_buckets_)
267 ✗ return false;
268
269 ✗ s3fanout::JobInfo *info = CreateJobInfo("");
270 ✗ info->request = s3fanout::JobInfo::kReqPutBucket;
271 ✗ std::string request_content;
272 ✗ if (!region_.empty()) {
273 ✗ request_content = std::string("<CreateBucketConfiguration xmlns="
274 "\"http://s3.amazonaws.com/doc/2006-03-01/\">"
275 "<LocationConstraint>")
276 ✗ + region_
277 ✗ + "</LocationConstraint>"
278 ✗ "</CreateBucketConfiguration>";
279 ✗ info->origin->Append(request_content.data(), request_content.length());
280 ✗ info->origin->Commit();
281 }
282
283 ✗ RequestCtrl req_ctrl;
284 ✗ MakePipe(req_ctrl.pipe_wait);
285 ✗ info->callback = const_cast<void *>(static_cast<void const *>(
286 ✗ MakeClosure(&S3Uploader::OnReqComplete, this, &req_ctrl)));
287
288 ✗ IncJobsInFlight();
289 ✗ UploadJobInfo(info);
290 ✗ req_ctrl.WaitFor();
291
292 ✗ return req_ctrl.return_code == 0;
293 }
294
295
296 10 unsigned int S3Uploader::GetNumberOfErrors() const {
297 10 return atomic_read32(&io_errors_);
298 }
299
300
301 /**
302 * Worker thread takes care of requesting new jobs and cleaning old ones.
303 */
304 65 void *S3Uploader::MainCollectResults(void *data) {
305 65 LogCvmfs(kLogUploadS3, kLogDebug, "Upload_S3 WorkerThread started.");
306 65 S3Uploader *uploader = reinterpret_cast<S3Uploader *>(data);
307
308 while (true) {
309 3165 s3fanout::JobInfo *info = uploader->s3fanout_mgr_->PopCompletedJob();
310
2/2
✓ Branch 0 taken 65 times.
✓ Branch 1 taken 3100 times.
3165 if (!info)
311 65 break;
312 // Report completed job
313 3100 int reply_code = 0;
314
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 3090 times.
3100 if (info->error_code != s3fanout::kFailOk) {
315
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if ((info->request != s3fanout::JobInfo::kReqHeadOnly)
316
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 || (info->error_code != s3fanout::kFailNotFound)) {
317 ✗ if (info->request == s3fanout::JobInfo::kReqDeleteMulti) {
318 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
319 "Batch delete of %lu objects failed. (error code: %d - %s)",
320 ✗ info->multi_delete_keys.size(), info->error_code,
321 s3fanout::Code2Ascii(info->error_code));
322 } else {
323 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
324 "Upload job for '%s' failed. (error code: %d - %s)",
325 ✗ info->object_key.c_str(), info->error_code,
326 s3fanout::Code2Ascii(info->error_code));
327 }
328 ✗ reply_code = 99;
329 ✗ atomic_inc32(&uploader->io_errors_);
330 }
331 }
332
2/2
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 3065 times.
3100 if (info->request == s3fanout::JobInfo::kReqDeleteMulti) {
333 // Parse response for per-key errors
334 35 std::set<std::string> failed_keys;
335 70 if (info->error_code == s3fanout::kFailOk
336
3/6
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 35 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 35 times.
35 && !info->response_body.empty()) {
337 ✗ std::vector<std::string> error_keys, error_codes, error_messages;
338 ✗ const unsigned num_errors = s3fanout::ParseDeleteMultiResponse(
339 ✗ info->response_body, &error_keys, &error_codes, &error_messages);
340 ✗ for (unsigned i = 0; i < num_errors; ++i) {
341 ✗ LogCvmfs(kLogUploadS3, kLogStderr,
342 "S3 multi-delete error for key '%s': %s - %s",
343 ✗ error_keys[i].c_str(), error_codes[i].c_str(),
344 ✗ error_messages[i].c_str());
345 ✗ atomic_inc32(&uploader->io_errors_);
346 ✗ failed_keys.insert(error_keys[i]);
347 }
348 }
349 // Decrement jobs_in_flight_ once for the entire batch.
350
1/2
✓ Branch 1 taken 35 times.
✗ Branch 2 not taken.
35 uploader->Respond(
351 NULL,
352
1/2
✓ Branch 1 taken 35 times.
✗ Branch 2 not taken.
70 UploaderResults(UploaderResults::kRemove,
353
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 35 times.
35 (info->error_code != s3fanout::kFailOk) ? 99 : 0));
354
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3065 times.
3100 } else if (info->request == s3fanout::JobInfo::kReqDelete) {
355 ✗ uploader->Respond(NULL, UploaderResults());
356
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 3040 times.
3065 } else if (info->request == s3fanout::JobInfo::kReqHeadOnly) {
357
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 15 times.
25 if (info->error_code == s3fanout::kFailNotFound)
358 10 reply_code = 1;
359
1/2
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
25 uploader->Respond(static_cast<CallbackTN *>(info->callback),
360 50 UploaderResults(UploaderResults::kLookup, reply_code));
361 } else {
362
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3040 times.
3040 if (info->request == s3fanout::JobInfo::kReqHeadPut) {
363 // The HEAD request was not transformed into a PUT request, thus this
364 // was a duplicate
365 // Uploaded catalogs are always unique ->
366 // assume this was a regular file and decrease appropriate counters
367 ✗ uploader->CountDuplicates();
368 ✗ uploader->DecUploadedChunks();
369 ✗ uploader->CountUploadedBytes(-(info->payload_size));
370 }
371 6080 uploader->Respond(
372
1/2
✓ Branch 1 taken 3040 times.
✗ Branch 2 not taken.
3040 static_cast<CallbackTN *>(info->callback),
373 6080 UploaderResults(UploaderResults::kChunkCommit, reply_code));
374
375
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3040 times.
3040 assert(info->origin.get() == nullptr);
376 }
377
1/2
✓ Branch 0 taken 3100 times.
✗ Branch 1 not taken.
3100 delete info;
378 3100 }
379
380 65 LogCvmfs(kLogUploadS3, kLogDebug, "Upload_S3 WorkerThread finished.");
381 65 return NULL;
382 }
383
384
385 2535 void S3Uploader::DoUpload(const std::string &remote_path,
386 IngestionSource *source,
387 const CallbackTN *callback) {
388
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 bool rvb = source->Open();
389
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2535 times.
2535 if (!rvb) {
390 ✗ Respond(callback, UploaderResults(100, source->GetPath()));
391 ✗ return;
392 }
393 uint64_t size;
394
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 rvb = source->GetSize(&size);
395
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2535 times.
2535 assert(rvb);
396
397 2535 FileBackedBuffer *origin = FileBackedBuffer::Create(
398
1/2
✓ Branch 2 taken 2535 times.
✗ Branch 3 not taken.
2535 kInMemoryObjectThreshold, spooler_definition().temporary_path);
399
400 unsigned char buffer[kPageSize];
401 ssize_t nbytes;
402 do {
403
1/2
✓ Branch 1 taken 995835 times.
✗ Branch 2 not taken.
995835 nbytes = source->Read(buffer, kPageSize);
404
2/2
✓ Branch 0 taken 994155 times.
✓ Branch 1 taken 1680 times.
995835 if (nbytes > 0)
405
1/2
✓ Branch 1 taken 994155 times.
✗ Branch 2 not taken.
994155 origin->Append(buffer, nbytes);
406
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 995835 times.
995835 if (nbytes < 0) {
407 ✗ source->Close();
408 ✗ delete origin;
409 ✗ Respond(callback, UploaderResults(100, source->GetPath()));
410 ✗ return;
411 }
412
2/2
✓ Branch 0 taken 993300 times.
✓ Branch 1 taken 2535 times.
995835 } while (nbytes == kPageSize);
413
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 source->Close();
414
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 origin->Commit();
415
416 s3fanout::JobInfo *info = new s3fanout::JobInfo(
417
2/4
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2535 times.
✗ Branch 5 not taken.
5070 repository_alias_ + "/" + remote_path,
418 const_cast<void *>(static_cast<void const *>(callback)),
419
2/4
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2535 times.
✗ Branch 5 not taken.
2535 origin);
420
421
3/6
✓ Branch 2 taken 2535 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 2535 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 2535 times.
2535 if (HasPrefix(remote_path, ".cvmfs", false /*ignore_case*/)) {
422 ✗ info->request = s3fanout::JobInfo::kReqPutDotCvmfs;
423
3/6
✓ Branch 2 taken 2535 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 2535 times.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 2535 times.
2535 } else if (HasSuffix(remote_path, ".html", false)) {
424 ✗ info->request = s3fanout::JobInfo::kReqPutHtml;
425 } else {
426
1/2
✓ Branch 0 taken 2535 times.
✗ Branch 1 not taken.
2535 if (peek_before_put_)
427 2535 info->request = s3fanout::JobInfo::kReqHeadPut;
428 }
429
430 2535 RequestCtrl req_ctrl;
431
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 MakePipe(req_ctrl.pipe_wait);
432 2535 req_ctrl.callback_forward = callback;
433
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 req_ctrl.original_path = source->GetPath();
434 2535 info->callback = const_cast<void *>(static_cast<void const *>(
435
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 MakeClosure(&S3Uploader::OnReqComplete, this, &req_ctrl)));
436
437
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 UploadJobInfo(info);
438
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 req_ctrl.WaitFor();
439
1/2
✓ Branch 2 taken 2535 times.
✗ Branch 3 not taken.
2535 LogCvmfs(kLogUploadS3, kLogDebug, "Uploading from source finished: %s",
440
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
5070 source->GetPath().c_str());
441 2535 }
442
443
444 3065 void S3Uploader::UploadJobInfo(s3fanout::JobInfo *info) {
445 3065 LogCvmfs(kLogUploadS3, kLogDebug,
446 "Uploading:\n"
447 "--> Object: '%s'\n"
448 "--> Bucket: '%s'\n"
449 "--> Host: '%s'\n",
450 info->object_key.c_str(), bucket_.c_str(), host_name_port_.c_str());
451
452 3065 s3fanout_mgr_->PushNewJob(info);
453 3065 }
454
455
456 505 UploadStreamHandle *S3Uploader::InitStreamedUpload(const CallbackTN *callback) {
457 return new S3StreamHandle(callback, kInMemoryObjectThreshold,
458
1/2
✓ Branch 3 taken 505 times.
✗ Branch 4 not taken.
505 spooler_definition().temporary_path);
459 }
460
461
462 4005 void S3Uploader::StreamedUpload(UploadStreamHandle *handle,
463 UploadBuffer buffer,
464 const CallbackTN *callback) {
465 4005 S3StreamHandle *s3_handle = static_cast<S3StreamHandle *>(handle);
466
467 4005 s3_handle->buffer->Append(buffer.data, buffer.size);
468
1/2
✓ Branch 2 taken 4010 times.
✗ Branch 3 not taken.
4010 Respond(callback, UploaderResults(UploaderResults::kBufferUpload, 0));
469 4010 }
470
471
472 505 void S3Uploader::FinalizeStreamedUpload(UploadStreamHandle *handle,
473 const shash::Any &content_hash) {
474 505 S3StreamHandle *s3_handle = static_cast<S3StreamHandle *>(handle);
475
476 // New file name based on content hash or remote_path override
477 505 std::string final_path;
478
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 505 times.
505 if (s3_handle->remote_path != "") {
479 ✗ final_path = repository_alias_ + "/" + s3_handle->remote_path;
480 } else {
481
3/6
✓ Branch 1 taken 505 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 505 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 505 times.
✗ Branch 8 not taken.
505 final_path = repository_alias_ + "/data/" + content_hash.MakePath();
482 }
483
484
1/2
✓ Branch 2 taken 505 times.
✗ Branch 3 not taken.
505 s3_handle->buffer->Commit();
485
486
1/2
✓ Branch 2 taken 505 times.
✗ Branch 3 not taken.
505 const size_t bytes_uploaded = s3_handle->buffer->GetSize();
487
488 s3fanout::JobInfo *info = new s3fanout::JobInfo(
489 final_path,
490 505 const_cast<void *>(static_cast<void const *>(handle->commit_callback)),
491
2/4
✓ Branch 2 taken 505 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 505 times.
✗ Branch 6 not taken.
505 s3_handle->buffer.release());
492
493
1/2
✓ Branch 0 taken 505 times.
✗ Branch 1 not taken.
505 if (peek_before_put_)
494 505 info->request = s3fanout::JobInfo::kReqHeadPut;
495
1/2
✓ Branch 1 taken 505 times.
✗ Branch 2 not taken.
505 UploadJobInfo(info);
496
497 // Remove the temporary file
498
1/2
✓ Branch 0 taken 505 times.
✗ Branch 1 not taken.
505 delete s3_handle;
499
500 // Update statistics counters
501 505 if (!content_hash.HasSuffix()
502
5/6
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 500 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 500 times.
✓ Branch 5 taken 5 times.
505 || content_hash.suffix == shash::kSuffixPartial) {
503
1/2
✓ Branch 1 taken 500 times.
✗ Branch 2 not taken.
500 CountUploadedChunks();
504
1/2
✓ Branch 1 taken 500 times.
✗ Branch 2 not taken.
500 CountUploadedBytes(bytes_uploaded);
505
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 } else if (content_hash.suffix == shash::kSuffixCatalog) {
506 ✗ CountUploadedCatalogs();
507 ✗ CountUploadedCatalogBytes(bytes_uploaded);
508 }
509 505 }
510
511
512 25 s3fanout::JobInfo *S3Uploader::CreateJobInfo(const std::string &path) const {
513
2/4
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 25 times.
✗ Branch 6 not taken.
25 FileBackedBuffer *buf = FileBackedBuffer::Create(kInMemoryObjectThreshold);
514
1/2
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 return new s3fanout::JobInfo(path, NULL, buf);
515 }
516
517
518 3005 void S3Uploader::DoRemoveAsync(const std::string &file_to_delete) {
519
2/4
✓ Branch 1 taken 3005 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 3005 times.
✗ Branch 5 not taken.
3005 const std::string mangled_path = repository_alias_ + "/" + file_to_delete;
520
521
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3005 times.
3005 if (!batch_delete_enabled_) {
522 ✗ s3fanout::JobInfo *info = CreateJobInfo(mangled_path);
523 ✗ info->request = s3fanout::JobInfo::kReqDelete;
524 ✗ LogCvmfs(kLogUploadS3, kLogDebug, "Asynchronously removing %s/%s",
525 bucket_.c_str(), info->object_key.c_str());
526 ✗ s3fanout_mgr_->PushNewJob(info);
527 ✗ return;
528 }
529
530 // Batch delete: collect keys and flush when the S3 batch limit is reached.
531 //
532 // The caller (RemoveAsync) already incremented jobs_in_flight_ for this key.
533 // We undo that immediately: for batched deletes, jobs_in_flight_ is managed
534 // per-batch rather than per-key. FlushDeleteBatch() increments once when
535 // the batch is dispatched, and MainCollectResults decrements once when the
536 // batch response arrives. This avoids a deadlock where jobs_in_flight_
537 // saturates (e.g. at 512) before the batch threshold (1000) is reached,
538 // blocking forever on a flush that never happens.
539
1/2
✓ Branch 1 taken 3005 times.
✗ Branch 2 not taken.
3005 DecJobsInFlight();
540 3005 const MutexLockGuard guard(delete_batch_mutex_);
541
1/2
✓ Branch 1 taken 3005 times.
✗ Branch 2 not taken.
3005 pending_deletes_.push_back(mangled_path);
542
2/2
✓ Branch 1 taken 30 times.
✓ Branch 2 taken 2975 times.
3005 if (pending_deletes_.size() >= batch_delete_size_) {
543
1/2
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
30 FlushDeleteBatch();
544 }
545
1/2
✓ Branch 2 taken 3005 times.
✗ Branch 3 not taken.
3005 }
546
547
548 95 void S3Uploader::FlushDeleteBatch() const {
549 // Caller must hold delete_batch_mutex_
550
2/2
✓ Branch 1 taken 60 times.
✓ Branch 2 taken 35 times.
95 if (pending_deletes_.empty())
551 60 return;
552
553
1/2
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
35 LogCvmfs(kLogUploadS3, kLogDebug, "Flushing batch delete of %lu objects",
554 pending_deletes_.size());
555
556 // Build XML request body
557
1/2
✓ Branch 1 taken 35 times.
✗ Branch 2 not taken.
35 const std::string xml = s3fanout::ComposeDeleteMultiXml(pending_deletes_);
558
2/4
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 35 times.
✗ Branch 6 not taken.
35 FileBackedBuffer *buf = FileBackedBuffer::Create(kInMemoryObjectThreshold);
559
1/2
✓ Branch 3 taken 35 times.
✗ Branch 4 not taken.
35 buf->Append(xml.data(), xml.length());
560
1/2
✓ Branch 1 taken 35 times.
✗ Branch 2 not taken.
35 buf->Commit();
561
562 // The object_key for multi-delete is empty (URL is bucket root + ?delete)
563
3/6
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 35 times.
✗ Branch 6 not taken.
✓ Branch 8 taken 35 times.
✗ Branch 9 not taken.
35 s3fanout::JobInfo *info = new s3fanout::JobInfo("", NULL, buf);
564 35 info->request = s3fanout::JobInfo::kReqDeleteMulti;
565 35 info->multi_delete_keys.swap(pending_deletes_);
566
567 // Track this batch as a single job in flight. MainCollectResults will
568 // call Respond() exactly once for kReqDeleteMulti to balance it.
569
1/2
✓ Branch 1 taken 35 times.
✗ Branch 2 not taken.
35 IncJobsInFlight();
570
1/2
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
35 s3fanout_mgr_->PushNewJob(info);
571 35 }
572
573
574 65 void S3Uploader::WaitForUpload() const {
575 {
576 65 const MutexLockGuard guard(delete_batch_mutex_);
577
1/2
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
65 FlushDeleteBatch();
578 65 }
579 65 AbstractUploader::WaitForUpload();
580 65 }
581
582
583 2560 void S3Uploader::OnReqComplete(const upload::UploaderResults &results,
584 RequestCtrl *ctrl) {
585 2560 ctrl->return_code = results.return_code;
586
2/2
✓ Branch 0 taken 2535 times.
✓ Branch 1 taken 25 times.
2560 if (ctrl->callback_forward != NULL) {
587 // We are already in Respond() so we must not call it again
588 2535 const upload::UploaderResults fix_path(results.return_code,
589
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 ctrl->original_path);
590
1/2
✓ Branch 1 taken 2535 times.
✗ Branch 2 not taken.
2535 (*(ctrl->callback_forward))(fix_path);
591
1/2
✓ Branch 0 taken 2535 times.
✗ Branch 1 not taken.
2535 delete ctrl->callback_forward;
592 2535 ctrl->callback_forward = NULL;
593 2535 }
594 2560 char c = 'c';
595
1/2
✓ Branch 1 taken 2560 times.
✗ Branch 2 not taken.
2560 WritePipe(ctrl->pipe_wait[1], &c, 1);
596 2560 }
597
598
599 25 bool S3Uploader::Peek(const std::string &path) {
600
2/4
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
25 const std::string mangled_path = repository_alias_ + "/" + path;
601
1/2
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
25 s3fanout::JobInfo *info = CreateJobInfo(mangled_path);
602
603 25 RequestCtrl req_ctrl;
604
1/2
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
25 MakePipe(req_ctrl.pipe_wait);
605 25 info->request = s3fanout::JobInfo::kReqHeadOnly;
606 25 info->callback = const_cast<void *>(static_cast<void const *>(
607
1/2
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
25 MakeClosure(&S3Uploader::OnReqComplete, this, &req_ctrl)));
608
609
1/2
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
25 IncJobsInFlight();
610
1/2
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
25 UploadJobInfo(info);
611
1/2
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
25 req_ctrl.WaitFor();
612
613 25 return req_ctrl.return_code == 0;
614 25 }
615
616
617 // noop: no mkdir needed in S3 storage
618 ✗ bool S3Uploader::Mkdir(const std::string &path) { return true; }
619
620
621 ✗ bool S3Uploader::PlaceBootstrappingShortcut(const shash::Any &object) {
622 ✗ return false; // TODO(rmeusel): implement
623 }
624
625
626 ✗ int64_t S3Uploader::DoGetObjectSize(const std::string &file_name) {
627 // TODO(dosarudaniel): use a head request for byte count
628 // Re-enable 661 integration test when done
629 ✗ return -EOPNOTSUPP;
630 }
631
632 } // namespace upload
633