quill
BackendWorkerLock.h
1 
7 #pragma once
8 
9 #if defined(_WIN32)
10  #if !defined(WIN32_LEAN_AND_MEAN)
11  #define WIN32_LEAN_AND_MEAN
12  #endif
13 
14  #if !defined(NOMINMAX)
15  // Mingw already defines this, so no need to redefine
16  #define NOMINMAX
17  #endif
18 
19  #include <windows.h>
20 #elif defined(__ANDROID__)
21 // No lock support on Android (no /tmp)
22 #else
23  #include <cerrno>
24  #include <cstring>
25  #include <fcntl.h>
26  #include <sys/file.h>
27  #include <unistd.h>
28 #endif
29 
30 #include "quill/core/Attributes.h"
31 #include "quill/core/QuillError.h"
32 #include "quill/core/ThreadPrimitives.h"
33 
34 #include <string>
35 
36 QUILL_BEGIN_NAMESPACE
37 
38 namespace detail
39 {
55 {
56 public:
57  /***/
58  explicit BackendWorkerLock(std::string const& pid)
59  {
60 #if defined(_WIN32)
61  std::string name = "Local\\QuillBackendLock" + pid;
62 
63  // Create a named mutex. If it already exists in this process, the flag ERROR_ALREADY_EXISTS is set.
64  _handle = CreateMutexA(nullptr, TRUE, name.data());
65 
66  if (_handle == nullptr)
67  {
68  QUILL_THROW(QuillError{"Failed to create mutex '" + name + "'"});
69  }
70 
71  if (GetLastError() == ERROR_ALREADY_EXISTS)
72  {
73  // Another instance in the same process already holds the lock.
74  CloseHandle(_handle);
75  _handle = nullptr;
76 
77  QUILL_THROW(QuillError{
78  "Duplicate backend worker thread detected. This indicates that the logging library has "
79  "been compiled into multiple binary modules (for instance, one module using a static build "
80  "and another using a shared build), resulting in separate instances of the backend worker. "
81  "Please build and link the logging library uniformly as a shared library with exported "
82  "symbols to ensure a single backend instance."});
83  }
84 #elif defined(__ANDROID__)
85  // disabled
86 #else
87  std::string path = "/tmp/QuillBackendLock" + pid;
88 
89  // Open or create the lock file. The file itself is just a vessel for the kernel lock.
90  // Retry on EINTR and transient errors. The lock is a diagnostic aid — if we ultimately
91  // cannot open the file, skip the check rather than crashing the application.
92  constexpr int max_retries{3};
93  constexpr uint64_t retry_delay_ns{100000000}; // 100 ms
94 
95  int fd{-1};
96  for (int attempt = 0; attempt < max_retries; ++attempt)
97  {
98  do
99  {
100  fd = open(path.data(), O_CREAT | O_RDWR, 0644);
101  } while (fd == -1 && errno == EINTR);
102 
103  if (fd != -1)
104  {
105  break;
106  }
107 
108  if (attempt < max_retries - 1)
109  {
110  sleep_for_ns(retry_delay_ns);
111  }
112  }
113 
114  if (fd == -1)
115  {
116  return;
117  }
118 
119  _fd = fd;
120  _path = path;
121 
122  // Try to acquire an exclusive lock without blocking.
123  // Each open() creates a separate file description, so two BackendWorkerLock instances
124  // in the same process get independent locks. flock returns -1 with errno EWOULDBLOCK
125  // if the lock is already held.
126  // Retry on EINTR and transient errors, but EWOULDBLOCK means a genuine duplicate.
127  int flock_err{0};
128  for (int attempt = 0; attempt < max_retries; ++attempt)
129  {
130  int ret{0};
131  do
132  {
133  ret = flock(_fd, LOCK_EX | LOCK_NB);
134  } while (ret != 0 && errno == EINTR);
135 
136  if (ret == 0)
137  {
138  flock_err = 0;
139  break;
140  }
141 
142  flock_err = errno;
143 
144  if (flock_err == EWOULDBLOCK)
145  {
146  // The lock is genuinely held by another instance — no point retrying.
147  break;
148  }
149 
150  if (attempt < max_retries - 1)
151  {
152  sleep_for_ns(retry_delay_ns);
153  }
154  }
155 
156  if (flock_err != 0)
157  {
158  close(_fd);
159  _fd = -1;
160 
161  if (flock_err == EWOULDBLOCK)
162  {
163  QUILL_THROW(QuillError{
164  "Duplicate backend worker thread detected. This indicates that the logging library has "
165  "been compiled into multiple binary modules (for instance, one module using a static "
166  "build "
167  "and another using a shared build), resulting in separate instances of the backend "
168  "worker. "
169  "Please build and link the logging library uniformly as a shared library with exported "
170  "symbols to ensure a single backend instance."});
171  }
172 
173  // For any other flock() error (e.g. EIO, ENOMEM, ENOLCK) that persists after
174  // retries, the lock is only a diagnostic aid. Skip the check rather than
175  // crashing the application.
176  }
177 #endif
178  }
179 
180  /***/
182  {
183 #if defined(_WIN32)
184  if (_handle != nullptr)
185  {
186  ReleaseMutex(_handle);
187  CloseHandle(_handle);
188  _handle = nullptr;
189  }
190 #elif defined(__ANDROID__)
191  // disabled
192 #else
193  if (_fd != -1)
194  {
195  // Unlink while the lock is still held so the file is removed before anyone else
196  // can acquire the same path. On kill -9 the kernel releases the flock but the
197  // file remains on disk — harmless, /tmp is cleaned on reboot.
198  unlink(_path.data());
199 
200  // Closing the fd releases the flock automatically.
201  close(_fd);
202  _fd = -1;
203  }
204 #endif
205  }
206 
207  // Disable copy and assignment.
208  BackendWorkerLock(BackendWorkerLock const&) = delete;
209  BackendWorkerLock& operator=(BackendWorkerLock const&) = delete;
210 
211 private:
212 #if defined(_WIN32)
213  HANDLE _handle{nullptr};
214 #elif defined(__ANDROID__)
215  // disabled
216 #else
217  int _fd{-1};
218  std::string _path;
219 #endif
220 };
221 } // namespace detail
222 
223 QUILL_END_NAMESPACE
QUILL_ATTRIBUTE_HOT void sleep_for_ns(uint64_t ns) noexcept
Mirrors std::this_thread::sleep_for(std::chrono::nanoseconds{ns}).
Definition: ThreadPrimitives.h:43
Ensures that only one instance of the backend worker is active.
Definition: BackendWorkerLock.h:54
Setups a signal handler to handle fatal signals.
Definition: BackendManager.h:28
custom exception
Definition: QuillError.h:47