GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_gc.cc
Date: 2026-10-25 02:40:14
Exec Total Coverage
Lines: 0 177 0.0%
Branches: 0 106 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 *
4 * This command processes a repository's catalog structure to detect and remove
5 * outdated and/or unneeded data objects.
6 */
7
8
9 #include "swissknife_gc.h"
10
11 #include <string>
12
13 #include "garbage_collection/garbage_collector.h"
14 #include "garbage_collection/gc_aux.h"
15 #include "garbage_collection/hash_filter.h"
16 #include "manifest.h"
17 #include "path_filters/inclusion_spec.h"
18 #include "reflog.h"
19 #include "statistics_database.h"
20 #include "upload_facility.h"
21 #include "util/posix.h"
22 #include "util/string.h"
23
24 namespace swissknife {
25
26 typedef HttpObjectFetcher<> ObjectFetcher;
27 typedef CatalogTraversalParallel<ObjectFetcher> ReadonlyCatalogTraversal;
28 typedef SmallhashFilter HashFilter;
29 typedef GarbageCollector<ReadonlyCatalogTraversal, HashFilter> GC;
30 typedef GarbageCollectorAux<ReadonlyCatalogTraversal, HashFilter> GCAux;
31 typedef GC::Configuration GcConfig;
32
33
34 ✗ ParameterList CommandGc::GetParams() const {
35 ✗ ParameterList r;
36 ✗ r.push_back(Parameter::Mandatory('r', "repository url"));
37 ✗ r.push_back(Parameter::Mandatory('u', "spooler definition string"));
38 ✗ r.push_back(Parameter::Mandatory('n', "fully qualified repository name"));
39 ✗ r.push_back(Parameter::Mandatory('R', "path to reflog.chksum file"));
40 ✗ r.push_back(Parameter::Optional('h', "conserve <h> revisions"));
41 ✗ r.push_back(Parameter::Optional('z', "conserve revisions younger than <z>"));
42 ✗ r.push_back(Parameter::Optional('k', "repository master key(s) / dir"));
43 ✗ r.push_back(Parameter::Optional('t', "temporary directory"));
44 ✗ r.push_back(Parameter::Optional('L', "path to deletion log file"));
45 ✗ r.push_back(Parameter::Optional('N', "number of threads to use"));
46 ✗ r.push_back(Parameter::Optional('@', "proxy url"));
47 ✗ r.push_back(Parameter::Switch('d', "dry run"));
48 ✗ r.push_back(Parameter::Switch('l', "list objects to be removed"));
49 ✗ r.push_back(Parameter::Switch('I', "upload updated statistics DB file"));
50 ✗ r.push_back(Parameter::Switch(
51 'p', "prune excluded objects on a partial replica (requires -E)"));
52 ✗ r.push_back(
53 ✗ Parameter::Optional('E', "inclusion spec for partial replication"));
54 ✗ return r;
55 }
56
57
58 ✗ int CommandGc::Main(const ArgumentList &args) {
59 ✗ const std::string start_time = GetGMTimestamp();
60
61 ✗ const std::string &repo_url = *args.find('r')->second;
62 ✗ const std::string &spooler = *args.find('u')->second;
63 ✗ const std::string &repo_name = *args.find('n')->second;
64 ✗ const std::string &reflog_chksum_path = *args.find('R')->second;
65 ✗ shash::Any reflog_hash;
66 ✗ if (!manifest::Reflog::ReadChecksum(reflog_chksum_path, &reflog_hash)) {
67 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Could not read reflog checksum");
68 ✗ return 1;
69 }
70
71 ✗ const uint64_t revisions = (args.count('h') > 0)
72 ✗ ? String2Int64(*args.find('h')->second)
73 ✗ : GcConfig::kFullHistory;
74 ✗ const time_t timestamp = (args.count('z') > 0)
75 ✗ ? static_cast<time_t>(
76 ✗ String2Int64(*args.find('z')->second))
77 ✗ : GcConfig::kNoTimestamp;
78 ✗ std::string repo_keys = (args.count('k') > 0) ? *args.find('k')->second : "";
79 ✗ if (DirectoryExists(repo_keys))
80 ✗ repo_keys = JoinStrings(FindFilesBySuffix(repo_keys, ".pub"), ":");
81 ✗ const bool dry_run = (args.count('d') > 0);
82 ✗ const bool list_condemned_objects = (args.count('l') > 0);
83 ✗ const std::string temp_directory = (args.count('t') > 0)
84 ✗ ? *args.find('t')->second
85 ✗ : "/tmp";
86 ✗ const std::string deletion_log_path = (args.count('L') > 0)
87 ✗ ? *args.find('L')->second
88 ✗ : "";
89 ✗ const bool upload_statsdb = (args.count('I') > 0);
90 ✗ const unsigned int num_threads = (args.count('N') > 0)
91 ✗ ? String2Uint64(*args.find('N')->second)
92 ✗ : 8;
93
94 ✗ const bool prune_partial_replica = (args.count('p') > 0);
95 ✗ if (prune_partial_replica && args.count('E') == 0) {
96 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "pruning requires an inclusion spec (-E)");
97 ✗ return 1;
98 }
99 ✗ std::unique_ptr<catalog::InclusionSpec> inclusion_spec;
100 ✗ if (args.count('E') > 0) {
101 ✗ inclusion_spec.reset(
102 ✗ catalog::InclusionSpec::Create(*args.find('E')->second));
103 ✗ if (!inclusion_spec || !inclusion_spec->IsValid()) {
104 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
105 "invalid partial replication inclusion spec");
106 ✗ return 1;
107 }
108 }
109
110 ✗ if (timestamp == GcConfig::kNoTimestamp
111 ✗ && revisions == GcConfig::kFullHistory) {
112 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
113 "neither a timestamp nor history threshold given");
114 ✗ return 1;
115 }
116
117 ✗ const bool follow_redirects = false;
118 ✗ const std::string proxy = ((args.count('@') > 0) ? *args.find('@')->second
119 ✗ : "");
120 ✗ if (!this->InitDownloadManager(follow_redirects, proxy)
121 ✗ || !this->InitSignatureManager(repo_keys)) {
122 ✗ LogCvmfs(kLogCatalog, kLogStderr, "failed to init repo connection");
123 ✗ return 1;
124 }
125
126 ObjectFetcher object_fetcher(repo_name,
127 repo_url,
128 temp_directory,
129 download_manager(),
130 ✗ signature_manager());
131
132 ✗ std::unique_ptr<manifest::Manifest> manifest;
133 ✗ const ObjectFetcher::Failures retval = object_fetcher.FetchManifest(
134 &manifest);
135 ✗ if (retval != ObjectFetcher::kFailOk) {
136 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
137 "failed to load repository manifest "
138 "(%d - %s)",
139 retval, Code2Ascii(retval));
140 ✗ return 1;
141 }
142
143 ✗ if (!manifest->garbage_collectable()) {
144 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
145 "repository does not allow garbage collection "
146 "(manifest does not have the garbage collection flag set; "
147 "ensure CVMFS_GARBAGE_COLLECTION=true in server.conf and "
148 "run a transaction + publish to update the manifest)");
149 ✗ return 1;
150 }
151
152 ✗ std::unique_ptr<manifest::Reflog> reflog;
153 ✗ reflog.reset(FetchReflog(&object_fetcher, repo_name, reflog_hash));
154 ✗ assert(reflog.get() != nullptr);
155
156 ✗ const upload::SpoolerDefinition spooler_definition(spooler, shash::kAny);
157 const std::unique_ptr<upload::AbstractUploader> uploader(
158 ✗ upload::AbstractUploader::Construct(spooler_definition));
159
160 ✗ if (uploader.get() == nullptr) {
161 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to initialize spooler for '%s'",
162 spooler.c_str());
163 ✗ return 1;
164 }
165
166 ✗ FILE *deletion_log_file = NULL;
167 ✗ if (!deletion_log_path.empty()) {
168 ✗ deletion_log_file = fopen(deletion_log_path.c_str(), "a+");
169 ✗ if (NULL == deletion_log_file) {
170 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
171 "failed to open deletion log file "
172 "(errno: %d)",
173 errno);
174 ✗ uploader->TearDown();
175 ✗ return 1;
176 }
177 }
178
179 ✗ const bool extended_stats = StatisticsDatabase::GcExtendedStats(repo_name);
180
181 ✗ reflog->BeginTransaction();
182
183 ✗ GcConfig config;
184 ✗ config.uploader = uploader.get();
185 ✗ config.keep_history_depth = revisions;
186 ✗ config.keep_history_timestamp = timestamp;
187 ✗ config.dry_run = dry_run;
188 ✗ config.verbose = list_condemned_objects;
189 ✗ config.object_fetcher = &object_fetcher;
190 ✗ config.reflog = reflog.get();
191 ✗ config.deleted_objects_logfile = deletion_log_file;
192 ✗ config.statistics = statistics();
193 ✗ config.extended_stats = extended_stats;
194 ✗ config.num_threads = num_threads;
195 ✗ config.prune_partial_replica = prune_partial_replica;
196 ✗ config.inclusion_spec = inclusion_spec.get();
197
198 ✗ if (deletion_log_file != NULL) {
199 ✗ const int bytes_written = fprintf(deletion_log_file,
200 "# Garbage Collection started at %s\n",
201 ✗ StringifyTime(time(NULL), true).c_str());
202 ✗ if (bytes_written < 0) {
203 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
204 "failed to write to deletion log '%s' "
205 "(errno: %d)",
206 deletion_log_path.c_str(), errno);
207 ✗ uploader->TearDown();
208 ✗ return 1;
209 }
210 }
211
212 ✗ StatisticsDatabase *stats_db = StatisticsDatabase::OpenStandardDB(repo_name);
213
214 // File catalogs
215 ✗ GC collector(config);
216 ✗ collector.UseReflogTimestamps();
217 ✗ bool success = collector.Collect();
218
219 ✗ if (!success) {
220 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "garbage collection failed");
221 ✗ if (!dry_run) {
222 ✗ stats_db->StoreGCStatistics(this->statistics(), start_time, false);
223 ✗ if (upload_statsdb) {
224 ✗ stats_db->UploadStatistics(uploader.get());
225 }
226 }
227 ✗ uploader->TearDown();
228 ✗ return 1;
229 }
230
231 // Tag databases, meta infos, certificates
232 ✗ HashFilter preserved_objects;
233 ✗ preserved_objects.Fill(manifest->certificate());
234 ✗ preserved_objects.Fill(manifest->history());
235 ✗ preserved_objects.Fill(manifest->meta_info());
236 ✗ GCAux collector_aux(config);
237 ✗ success = collector_aux.CollectOlderThan(collector.oldest_trunk_catalog(),
238 preserved_objects);
239 ✗ if (!success) {
240 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
241 "garbage collection of auxiliary files failed");
242 ✗ if (!dry_run) {
243 ✗ stats_db->StoreGCStatistics(this->statistics(), start_time, false);
244 ✗ if (upload_statsdb) {
245 ✗ stats_db->UploadStatistics(uploader.get());
246 }
247 }
248 ✗ uploader->TearDown();
249 ✗ return 1;
250 }
251
252 // As of here: garbage collection succeeded, cleanup & commit
253
254 ✗ if (deletion_log_file != NULL) {
255 ✗ const int bytes_written = fprintf(deletion_log_file,
256 "# Garbage Collection finished at %s\n\n",
257 ✗ StringifyTime(time(NULL), true).c_str());
258 ✗ assert(bytes_written >= 0);
259 ✗ fclose(deletion_log_file);
260 }
261
262 ✗ reflog->CommitTransaction();
263 // Has to be outside the transaction
264 ✗ success = reflog->Vacuum();
265 ✗ assert(success);
266 ✗ reflog->DropDatabaseFileOwnership();
267 ✗ const std::string reflog_db = reflog->database_file();
268 ✗ reflog.reset();
269
270 ✗ if (!dry_run) {
271 ✗ uploader->UploadFile(reflog_db, ".cvmfsreflog");
272 ✗ manifest::Reflog::HashDatabase(reflog_db, &reflog_hash);
273 ✗ uploader->WaitForUpload();
274 ✗ manifest::Reflog::WriteChecksum(reflog_chksum_path, reflog_hash);
275 }
276
277 ✗ unlink(reflog_db.c_str());
278
279 ✗ if (uploader->GetNumberOfErrors() > 0 && !dry_run) {
280 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to upload updated Reflog");
281
282 ✗ stats_db->StoreGCStatistics(this->statistics(), start_time, false);
283 ✗ if (upload_statsdb) {
284 ✗ stats_db->UploadStatistics(uploader.get());
285 }
286
287 ✗ uploader->TearDown();
288 ✗ return 1;
289 }
290
291 ✗ if (!dry_run) {
292 ✗ stats_db->StoreGCStatistics(this->statistics(), start_time, true);
293 ✗ if (upload_statsdb) {
294 ✗ stats_db->UploadStatistics(uploader.get());
295 }
296 }
297 ✗ uploader->TearDown();
298 ✗ return 0;
299 }
300
301 } // namespace swissknife
302