blob: ad97f7d6d5a1c8b92fa63ef37f1141debc6ab6d0 [file] [log] [blame]
Alexei Strots01395492023-03-20 13:59:56 -07001#include "aos/events/logging/log_backend.h"
2
3#include <dirent.h>
4
5#include <filesystem>
6
Alexei Strotsf08b8fb2023-04-21 19:46:08 -07007#include "absl/strings/match.h"
Alexei Strots01395492023-03-20 13:59:56 -07008#include "absl/strings/str_cat.h"
Alexei Strots01395492023-03-20 13:59:56 -07009#include "glog/logging.h"
10
Alexei Strotsf08b8fb2023-04-21 19:46:08 -070011#include "aos/events/logging/file_operations.h"
Philipp Schrader790cb542023-07-05 21:06:52 -070012#include "aos/util/file.h"
13
Alexei Strots01395492023-03-20 13:59:56 -070014DEFINE_bool(direct, false,
15 "If true, write using O_DIRECT and write 512 byte aligned blocks "
16 "whenever possible.");
Austin Schuh3ebaf782023-04-07 16:03:28 -070017DEFINE_bool(
18 sync, false,
19 "If true, sync data to disk as we go so we don't get too far ahead. Also "
20 "fadvise that we are done with the memory once it hits disk.");
Alexei Strots01395492023-03-20 13:59:56 -070021
Alexei Strotsa0b99d72023-04-11 15:12:42 -070022DEFINE_uint32(queue_reserve, 32, "Pre-reserved size of write queue.");
23
Alexei Strots01395492023-03-20 13:59:56 -070024namespace aos::logger {
25namespace {
26constexpr const char *kTempExtension = ".tmp";
Alexei Strotsa0b99d72023-04-11 15:12:42 -070027
28// Assuming that kSector is power of 2, it aligns address to the left size.
29inline size_t AlignToLeft(size_t value) {
30 return value & (~(FileHandler::kSector - 1));
31}
32
33inline bool IsAligned(size_t value) {
34 return value % FileHandler::kSector == 0;
35}
36
37inline bool IsAlignedStart(const absl::Span<const uint8_t> span) {
38 return (reinterpret_cast<size_t>(span.data()) % FileHandler::kSector) == 0;
39}
40
41inline bool IsAlignedLength(const absl::Span<const uint8_t> span) {
42 return (span.size() % FileHandler::kSector) == 0;
43}
44
45} // namespace
46
47logger::QueueAligner::QueueAligner() {
48 aligned_queue_.reserve(FLAGS_queue_reserve);
49}
50
51void logger::QueueAligner::FillAlignedQueue(
52 const absl::Span<const absl::Span<const uint8_t>> &queue) {
53 aligned_queue_.clear();
54
55 for (const auto &span : queue) {
56 // Generally, every span might have 3 optional parts (i.e. 2^3 cases):
57 // 1. unaligned prefix - from start till first aligned block.
58 // 2. aligned main - block with aligned start and size
59 // 3. unaligned suffix - block with aligned start, and size less than one
60 // sector. If size of the span is less than 1 sector, let's call it prefix.
61
62 auto *data = span.data();
63 size_t size = span.size();
64 const auto start = reinterpret_cast<size_t>(data);
65 VLOG(2) << "Consider span starting at " << std::hex << start
66 << " with size " << size;
67
68 CHECK_GT(size, 0u) << ": Nobody should be sending empty messages.";
69
70 const auto next_aligned =
71 IsAligned(start) ? start : AlignToLeft(start) + FileHandler::kSector;
72 const auto prefix_size = next_aligned - start;
73 VLOG(2) << "Calculated prefix size " << std::hex << prefix_size;
74
75 if (prefix_size >= size) {
76 // size of prefix >= size of span - alignment is not possible, accept the
77 // whole span
78 VLOG(2) << "Only prefix found";
79 CHECK_GT(size, 0u);
80 aligned_queue_.emplace_back(data, size, false);
81 continue;
82 }
83 CHECK_LT(prefix_size, FileHandler::kSector)
84 << ": Wrong calculation of 'next' aligned position";
85 if (prefix_size > 0) {
86 // Cut the prefix and move to the main part.
87 VLOG(2) << "Cutting prefix at " << std::hex << start << " of size "
88 << prefix_size;
89 aligned_queue_.emplace_back(data, prefix_size, false);
90 data += prefix_size;
91 size -= prefix_size;
92 CHECK(data <= span.data() + span.size()) << " :Boundaries after prefix";
93 }
94
95 if (IsAligned(size)) {
96 // the rest is aligned.
97 VLOG(2) << "Returning aligned main part";
98 CHECK_GT(size, 0u);
99 aligned_queue_.emplace_back(data, size, true);
100 continue;
101 }
102
103 const auto aligned_size = AlignToLeft(size);
104 CHECK(aligned_size < size) << ": Wrong calculation of 'main' size";
105 if (aligned_size > 0) {
106 VLOG(2) << "Cutting main part starting " << std::hex
107 << reinterpret_cast<size_t>(data) << " of size " << aligned_size;
108 aligned_queue_.emplace_back(data, aligned_size, true);
109
110 data += aligned_size;
111 size -= aligned_size;
112 CHECK(data <= span.data() + span.size()) << " :Boundaries after main";
113 }
114
115 VLOG(2) << "Cutting suffix part starting " << std::hex
116 << reinterpret_cast<size_t>(data) << " of size " << size;
117 CHECK_GT(size, 0u);
118 aligned_queue_.emplace_back(data, size, false);
119 }
Alexei Strots01395492023-03-20 13:59:56 -0700120}
121
122FileHandler::FileHandler(std::string filename)
123 : filename_(std::move(filename)), supports_odirect_(FLAGS_direct) {}
124
125FileHandler::~FileHandler() { Close(); }
126
127WriteCode FileHandler::OpenForWrite() {
128 iovec_.reserve(10);
129 if (!aos::util::MkdirPIfSpace(filename_, 0777)) {
130 return WriteCode::kOutOfSpace;
131 } else {
132 fd_ = open(filename_.c_str(), O_RDWR | O_CLOEXEC | O_CREAT | O_EXCL, 0774);
133 if (fd_ == -1 && errno == ENOSPC) {
134 return WriteCode::kOutOfSpace;
135 } else {
136 PCHECK(fd_ != -1) << ": Failed to open " << filename_ << " for writing";
137 VLOG(1) << "Opened " << filename_ << " for writing";
138 }
139
140 flags_ = fcntl(fd_, F_GETFL, 0);
Alexei Strotsbc082d82023-05-03 08:43:42 -0700141 PCHECK(flags_ >= 0) << ": Failed to get flags for " << filename_;
Alexei Strots01395492023-03-20 13:59:56 -0700142
143 EnableDirect();
144
145 CHECK(std::filesystem::exists(filename_));
146
147 return WriteCode::kOk;
148 }
149}
150
151void FileHandler::EnableDirect() {
152 if (supports_odirect_ && !ODirectEnabled()) {
153 const int new_flags = flags_ | O_DIRECT;
154 // Track if we failed to set O_DIRECT. Note: Austin hasn't seen this call
155 // fail. The write call tends to fail instead.
156 if (fcntl(fd_, F_SETFL, new_flags) == -1) {
Alexei Strotsbc082d82023-05-03 08:43:42 -0700157 PLOG(WARNING) << "Failed to set O_DIRECT on " << filename_;
Alexei Strots01395492023-03-20 13:59:56 -0700158 supports_odirect_ = false;
159 } else {
Alexei Strotsbc082d82023-05-03 08:43:42 -0700160 VLOG(1) << "Enabled O_DIRECT on " << filename_;
Alexei Strots01395492023-03-20 13:59:56 -0700161 flags_ = new_flags;
162 }
163 }
164}
165
166void FileHandler::DisableDirect() {
167 if (supports_odirect_ && ODirectEnabled()) {
168 flags_ = flags_ & (~O_DIRECT);
169 PCHECK(fcntl(fd_, F_SETFL, flags_) != -1) << ": Failed to disable O_DIRECT";
Alexei Strotsbc082d82023-05-03 08:43:42 -0700170 VLOG(1) << "Disabled O_DIRECT on " << filename_;
Alexei Strots01395492023-03-20 13:59:56 -0700171 }
172}
173
174WriteResult FileHandler::Write(
175 const absl::Span<const absl::Span<const uint8_t>> &queue) {
176 iovec_.clear();
Austin Schuh3ebaf782023-04-07 16:03:28 -0700177 CHECK_LE(queue.size(), static_cast<size_t>(IOV_MAX));
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700178
179 queue_aligner_.FillAlignedQueue(queue);
180 CHECK_LE(queue_aligner_.aligned_queue().size(), static_cast<size_t>(IOV_MAX));
Alexei Strots01395492023-03-20 13:59:56 -0700181
182 // Ok, we now need to figure out if we were aligned, and if we were, how much
183 // of the data we are being asked to write is aligned.
184 //
Austin Schuh3ebaf782023-04-07 16:03:28 -0700185 // When writing with O_DIRECT, the kernel only will accept writes where the
186 // offset into the file is a multiple of kSector, the data is aligned to
187 // kSector in memory, and the length being written is a multiple of kSector.
188 // Some of the callers use an aligned ResizeableBuffer to generate 512 byte
189 // aligned buffers for this code to find and use.
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700190 bool was_aligned = IsAligned(total_write_bytes_);
191 VLOG(1) << "Started " << (was_aligned ? "aligned" : "unaligned")
192 << " at offset " << total_write_bytes_ << " on " << filename();
Austin Schuh9e8df9e2023-05-03 08:28:29 -0700193
Alexei Strotscaf17d32023-04-03 22:31:11 -0700194 // Walk through aligned queue and batch writes based on aligned flag
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700195 for (const auto &item : queue_aligner_.aligned_queue()) {
196 if (was_aligned != item.aligned) {
197 // Switching aligned context. Let's flush current batch.
198 if (!iovec_.empty()) {
199 // Flush current queue if we need.
200 const auto code = WriteV(iovec_, was_aligned);
Austin Schuh3ebaf782023-04-07 16:03:28 -0700201 if (code == WriteCode::kOutOfSpace) {
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700202 // We cannot say anything about what number of messages was written
203 // for sure.
Austin Schuh3ebaf782023-04-07 16:03:28 -0700204 return {
205 .code = code,
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700206 .messages_written = queue.size(),
Austin Schuh3ebaf782023-04-07 16:03:28 -0700207 };
208 }
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700209 iovec_.clear();
Austin Schuh3ebaf782023-04-07 16:03:28 -0700210 }
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700211 // Write queue is flushed. WriteV updates the total_write_bytes_.
212 was_aligned = IsAligned(total_write_bytes_) && item.aligned;
Alexei Strots01395492023-03-20 13:59:56 -0700213 }
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700214 iovec_.push_back(
215 {.iov_base = const_cast<uint8_t *>(item.data), .iov_len = item.size});
Alexei Strots01395492023-03-20 13:59:56 -0700216 }
217
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700218 WriteCode result_code = WriteCode::kOk;
219 if (!iovec_.empty()) {
220 // Flush current queue if we need.
221 result_code = WriteV(iovec_, was_aligned);
222 }
Alexei Strots01395492023-03-20 13:59:56 -0700223 return {
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700224 .code = result_code,
Austin Schuh3ebaf782023-04-07 16:03:28 -0700225 .messages_written = queue.size(),
Alexei Strots01395492023-03-20 13:59:56 -0700226 };
227}
228
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700229WriteCode FileHandler::WriteV(const std::vector<struct iovec> &iovec,
230 bool aligned) {
Alexei Strots01395492023-03-20 13:59:56 -0700231 // Configure the file descriptor to match the mode we should be in. This is
232 // safe to over-call since it only does the syscall if needed.
233 if (aligned) {
234 EnableDirect();
235 } else {
236 DisableDirect();
237 }
238
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700239 VLOG(2) << "Flushing queue of " << iovec.size() << " elements, "
240 << (aligned ? "aligned" : "unaligned");
241
242 CHECK_GT(iovec.size(), 0u);
Alexei Strots01395492023-03-20 13:59:56 -0700243 const auto start = aos::monotonic_clock::now();
Alexei Strots01395492023-03-20 13:59:56 -0700244
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700245 // Validation of alignment assumptions.
Austin Schuh9e8df9e2023-05-03 08:28:29 -0700246 if (aligned) {
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700247 CHECK(IsAligned(total_write_bytes_))
Austin Schuh9e8df9e2023-05-03 08:28:29 -0700248 << ": Failed after writing " << total_write_bytes_
249 << " to the file, attempting aligned write with unaligned start.";
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700250
251 for (const auto &iovec_item : iovec) {
Austin Schuh9e8df9e2023-05-03 08:28:29 -0700252 absl::Span<const uint8_t> data(
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700253 reinterpret_cast<const uint8_t *>(iovec_item.iov_base),
254 iovec_item.iov_len);
255 VLOG(2) << " iov_base " << static_cast<void *>(iovec_item.iov_base)
256 << ", iov_len " << iovec_item.iov_len;
Austin Schuh9e8df9e2023-05-03 08:28:29 -0700257 CHECK(IsAlignedStart(data) && IsAlignedLength(data));
Austin Schuh9e8df9e2023-05-03 08:28:29 -0700258 }
259 }
260
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700261 // Calculation of expected written size.
262 size_t counted_size = 0;
263 for (const auto &iovec_item : iovec) {
264 CHECK_GT(iovec_item.iov_len, 0u);
265 counted_size += iovec_item.iov_len;
Austin Schuh3ebaf782023-04-07 16:03:28 -0700266 }
267
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700268 VLOG(2) << "Going to write " << counted_size;
269 CHECK_GT(counted_size, 0u);
270
271 const ssize_t written = writev(fd_, iovec.data(), iovec.size());
272 VLOG(2) << "Wrote " << written << ", for iovec size " << iovec.size();
273
274 const auto end = aos::monotonic_clock::now();
275 if (written == -1 && errno == ENOSPC) {
276 return WriteCode::kOutOfSpace;
277 }
278 PCHECK(written >= 0) << ": write failed, got " << written;
279 if (written < static_cast<ssize_t>(counted_size)) {
280 // Sometimes this happens instead of ENOSPC. On a real filesystem, this
281 // never seems to happen in any other case. If we ever want to log to a
282 // socket, this will happen more often. However, until we get there, we'll
283 // just assume it means we ran out of space.
284 return WriteCode::kOutOfSpace;
285 }
Austin Schuh3ebaf782023-04-07 16:03:28 -0700286
Austin Schuh9e8df9e2023-05-03 08:28:29 -0700287 if (FLAGS_sync) {
Alexei Strots01395492023-03-20 13:59:56 -0700288 // Flush asynchronously and force the data out of the cache.
289 sync_file_range(fd_, total_write_bytes_, written, SYNC_FILE_RANGE_WRITE);
290 if (last_synced_bytes_ != 0) {
291 // Per Linus' recommendation online on how to do fast file IO, do a
292 // blocking flush of the previous write chunk, and then tell the kernel to
293 // drop the pages from the cache. This makes sure we can't get too far
294 // ahead.
295 sync_file_range(fd_, last_synced_bytes_,
296 total_write_bytes_ - last_synced_bytes_,
297 SYNC_FILE_RANGE_WAIT_BEFORE | SYNC_FILE_RANGE_WRITE |
298 SYNC_FILE_RANGE_WAIT_AFTER);
299 posix_fadvise(fd_, last_synced_bytes_,
300 total_write_bytes_ - last_synced_bytes_,
301 POSIX_FADV_DONTNEED);
Alexei Strots01395492023-03-20 13:59:56 -0700302 }
Austin Schuh3c4029f2023-04-13 12:09:55 -0700303 last_synced_bytes_ = total_write_bytes_;
Alexei Strots01395492023-03-20 13:59:56 -0700304 }
305
Alexei Strots01395492023-03-20 13:59:56 -0700306 total_write_bytes_ += written;
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700307 if (aligned) {
308 written_aligned_ += written;
309 }
Alexei Strotsbc082d82023-05-03 08:43:42 -0700310 WriteStatistics()->UpdateStats(end - start, written, iovec.size());
Alexei Strots01395492023-03-20 13:59:56 -0700311 return WriteCode::kOk;
312}
313
314WriteCode FileHandler::Close() {
315 if (!is_open()) {
316 return WriteCode::kOk;
317 }
318 bool ran_out_of_space = false;
319 if (close(fd_) == -1) {
320 if (errno == ENOSPC) {
321 ran_out_of_space = true;
322 } else {
323 PLOG(ERROR) << "Closing log file failed";
324 }
325 }
326 fd_ = -1;
327 VLOG(1) << "Closed " << filename_;
328 return ran_out_of_space ? WriteCode::kOutOfSpace : WriteCode::kOk;
329}
330
331FileBackend::FileBackend(std::string_view base_name)
332 : base_name_(base_name), separator_(base_name_.back() == '/' ? "" : "_") {}
333
Alexei Strotsbc082d82023-05-03 08:43:42 -0700334std::unique_ptr<LogSink> FileBackend::RequestFile(std::string_view id) {
Alexei Strots01395492023-03-20 13:59:56 -0700335 const std::string filename = absl::StrCat(base_name_, separator_, id);
336 return std::make_unique<FileHandler>(filename);
337}
338
Alexei Strotsf08b8fb2023-04-21 19:46:08 -0700339std::vector<std::string> FileBackend::ListFiles() const {
340 std::filesystem::path directory(base_name_);
341 if (!is_directory(directory)) {
342 directory = directory.parent_path();
343 }
344 internal::LocalFileOperations operations(directory.string());
345 std::vector<std::string> files;
346 operations.FindLogs(&files);
347
348 std::vector<std::string> names;
349 const std::string prefix = absl::StrCat(base_name_, separator_);
350 for (const auto &file : files) {
351 CHECK(absl::StartsWith(file, prefix));
352 names.push_back(file.substr(prefix.size()));
353 }
354 return names;
355}
356
357std::unique_ptr<DataDecoder> FileBackend::GetDecoder(
358 std::string_view id) const {
359 const std::string filename = absl::StrCat(base_name_, separator_, id);
360 CHECK(std::filesystem::exists(filename));
361 return std::make_unique<DummyDecoder>(filename);
362}
363
Alexei Strots01395492023-03-20 13:59:56 -0700364RenamableFileBackend::RenamableFileBackend(std::string_view base_name)
365 : base_name_(base_name), separator_(base_name_.back() == '/' ? "" : "_") {}
366
Alexei Strotsbc082d82023-05-03 08:43:42 -0700367std::unique_ptr<LogSink> RenamableFileBackend::RequestFile(
Alexei Strots01395492023-03-20 13:59:56 -0700368 std::string_view id) {
369 const std::string filename =
370 absl::StrCat(base_name_, separator_, id, temp_suffix_);
371 return std::make_unique<RenamableFileHandler>(this, filename);
372}
373
374void RenamableFileBackend::EnableTempFiles() {
375 use_temp_files_ = true;
376 temp_suffix_ = kTempExtension;
377}
378
379bool RenamableFileBackend::RenameLogBase(std::string_view new_base_name) {
380 if (new_base_name == base_name_) {
381 return true;
382 }
383 CHECK(old_base_name_.empty())
384 << "Only one change of base_name is supported. Was: " << old_base_name_;
385
386 std::string current_directory = base_name_;
387 std::string new_directory(new_base_name);
388
389 auto current_path_split = current_directory.rfind("/");
390 CHECK(current_path_split != std::string::npos)
391 << "Could not find / in the current directory path";
392 auto new_path_split = new_directory.rfind("/");
393 CHECK(new_path_split != std::string::npos)
394 << "Could not find / in the new directory path";
395
396 CHECK(new_base_name.substr(new_path_split) ==
397 current_directory.substr(current_path_split))
398 << "Rename of file base from " << current_directory << " to "
399 << new_directory << " is not supported.";
400
401 current_directory.resize(current_path_split);
402 new_directory.resize(new_path_split);
403 DIR *dir = opendir(current_directory.c_str());
404 if (dir) {
405 closedir(dir);
406 const int result = rename(current_directory.c_str(), new_directory.c_str());
407 if (result != 0) {
408 PLOG(ERROR) << "Unable to rename " << current_directory << " to "
409 << new_directory;
410 return false;
411 }
412 } else {
413 // Handle if directory was already renamed.
414 dir = opendir(new_directory.c_str());
415 if (!dir) {
416 LOG(ERROR) << "Old directory " << current_directory
417 << " missing and new directory " << new_directory
418 << " not present.";
419 return false;
420 }
421 closedir(dir);
422 }
423 old_base_name_ = base_name_;
424 base_name_ = std::string(new_base_name);
425 separator_ = base_name_.back() == '/' ? "" : "_";
426 return true;
427}
428
429WriteCode RenamableFileBackend::RenameFileAfterClose(
430 std::string_view filename) {
431 // Fast check that we can skip rename.
432 if (!use_temp_files_ && old_base_name_.empty()) {
433 return WriteCode::kOk;
434 }
435
436 std::string current_filename(filename);
437
438 // When changing the base name, we rename the log folder while there active
439 // buffer writers. Therefore, the name of that active buffer may still refer
440 // to the old file location rather than the new one.
441 if (!old_base_name_.empty()) {
442 auto offset = current_filename.find(old_base_name_);
443 if (offset != std::string::npos) {
444 current_filename.replace(offset, old_base_name_.length(), base_name_);
445 }
446 }
447
448 std::string final_filename = current_filename;
449 if (use_temp_files_) {
450 CHECK(current_filename.size() > temp_suffix_.size());
451 final_filename = current_filename.substr(
452 0, current_filename.size() - temp_suffix_.size());
453 }
454
455 int result = rename(current_filename.c_str(), final_filename.c_str());
456
457 bool ran_out_of_space = false;
458 if (result != 0) {
459 if (errno == ENOSPC) {
460 ran_out_of_space = true;
461 } else {
462 PLOG(FATAL) << "Renaming " << current_filename << " to " << final_filename
463 << " failed";
464 }
465 } else {
466 VLOG(1) << "Renamed " << current_filename << " -> " << final_filename;
467 }
468 return ran_out_of_space ? WriteCode::kOutOfSpace : WriteCode::kOk;
469}
470
471WriteCode RenamableFileBackend::RenamableFileHandler::Close() {
472 if (!is_open()) {
473 return WriteCode::kOk;
474 }
475 if (FileHandler::Close() == WriteCode::kOutOfSpace) {
476 return WriteCode::kOutOfSpace;
477 }
478 if (owner_->RenameFileAfterClose(filename()) == WriteCode::kOutOfSpace) {
479 return WriteCode::kOutOfSpace;
480 }
481 return WriteCode::kOk;
482}
Alexei Strotsa0b99d72023-04-11 15:12:42 -0700483
Austin Schuh3ebaf782023-04-07 16:03:28 -0700484} // namespace aos::logger