Coverage Report

Created: 2026-10-31 07:06

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/trafficserver/include/tsutil/TsSharedMutex.h
Line
Count
Source
1
/** @file
2
3
  A drop-in replacement for std::shared_mutex with guarantees against writer
4
  starvation.
5
6
  @section license License
7
8
  Licensed to the Apache Software Foundation (ASF) under one
9
  or more contributor license agreements.  See the NOTICE file
10
  distributed with this work for additional information
11
  regarding copyright ownership.  The ASF licenses this file
12
  to you under the Apache License, Version 2.0 (the
13
  "License"); you may not use this file except in compliance
14
  with the License.  You may obtain a copy of the License at
15
16
      http://www.apache.org/licenses/LICENSE-2.0
17
18
  Unless required by applicable law or agreed to in writing, software
19
  distributed under the License is distributed on an "AS IS" BASIS,
20
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
  See the License for the specific language governing permissions and
22
  limitations under the License.
23
 */
24
25
#pragma once
26
27
#include <cstdio>
28
#include <cstdlib>
29
#include <pthread.h>
30
#include "tsutil/Strerror.h"
31
#include "tsutil/Assert.h"
32
#include "tsutil/ts_thread_safety.h"
33
34
#ifdef X
35
#error "X preprocessor symbol defined"
36
#endif
37
38
#if !defined(__OPTIMIZE__)
39
40
#define X(P) P
41
42
#include <atomic>
43
44
#else
45
46
#define X(P)
47
48
#endif
49
50
namespace ts
51
{
52
// A class with the same interface as std::shared_mutex, but which is not prone to writer starvation.
53
//
54
class TS_CAPABILITY("shared_mutex") shared_mutex
55
{
56
public:
57
2
  shared_mutex() {}
58
59
  // No copying or moving.
60
  //
61
  shared_mutex(shared_mutex const &)            = delete;
62
  shared_mutex &operator=(shared_mutex const &) = delete;
63
64
  // The lock/unlock methods are the trusted implementation of this capability:
65
  // their bodies drive the raw pthread_rwlock_t, which some libc headers (e.g.
66
  // FreeBSD's <pthread.h>) annotate as a capability in its own right. Exempt the
67
  // bodies from analysis so only the capability contract on each signature is
68
  // checked; the actual data-race checking happens at the call sites.
69
  void
70
  lock() TS_ACQUIRE() TS_NO_THREAD_SAFETY_ANALYSIS
71
0
  {
72
0
    int error = pthread_rwlock_wrlock(&_lock);
73
0
    if (error != 0) {
74
0
      _call_fatal("pthread_rwlock_wrlock", &_lock, error);
75
0
    }
76
0
    X(_exclusive = true;)
77
0
  }
78
79
  bool
80
  try_lock() TS_TRY_ACQUIRE(true) TS_NO_THREAD_SAFETY_ANALYSIS
81
0
  {
82
0
    int error = pthread_rwlock_trywrlock(&_lock);
83
0
    if (EBUSY == error) {
84
0
      return false;
85
0
    }
86
0
    if (error != 0) {
87
0
      _call_fatal("pthread_rwlock_trywrlock", &_lock, error);
88
0
    }
89
0
    X(_exclusive = true;)
90
0
91
0
    return true;
92
0
  }
93
94
  void
95
  unlock() TS_RELEASE() TS_NO_THREAD_SAFETY_ANALYSIS
96
0
  {
97
0
    X(debug_assert(_exclusive);)
98
0
    X(_exclusive = false;)
99
100
0
    _unlock();
101
0
  }
102
103
  void
104
  lock_shared() TS_ACQUIRE_SHARED() TS_NO_THREAD_SAFETY_ANALYSIS
105
0
  {
106
0
    int error = pthread_rwlock_rdlock(&_lock);
107
0
    if (error != 0) {
108
0
      _call_fatal("pthread_rwlock_rdlock", &_lock, error);
109
0
    }
110
0
    X(debug_assert(_shared >= 0);)
111
0
    X(++_shared;)
112
0
    X(debug_assert(_shared > 0);)
113
0
  }
114
115
  bool
116
  try_lock_shared() TS_TRY_ACQUIRE_SHARED(true) TS_NO_THREAD_SAFETY_ANALYSIS
117
0
  {
118
0
    int error = pthread_rwlock_tryrdlock(&_lock);
119
0
    if (EBUSY == error) {
120
0
      return false;
121
0
    }
122
0
    if (error != 0) {
123
0
      _call_fatal("pthread_rwlock_tryrdlock", &_lock, error);
124
0
    }
125
0
126
0
    X(++_shared;)
127
0
128
0
    return true;
129
0
  }
130
131
  void
132
  unlock_shared() TS_RELEASE_SHARED() TS_NO_THREAD_SAFETY_ANALYSIS
133
0
  {
134
0
    X(debug_assert(_shared > 0);)
135
0
    X(--_shared;)
136
0
    X(debug_assert(_shared >= 0);)
137
138
0
    _unlock();
139
0
  }
140
141
  ~shared_mutex()
142
0
  {
143
0
    int error = pthread_rwlock_destroy(&_lock);
144
0
    if (error != 0) {
145
0
      _call_fatal("pthread_rwlock_destroy", &_lock, error);
146
0
    }
147
0
  }
148
149
  using native_handle_type = pthread_rwlock_t;
150
151
  native_handle_type
152
  native_handle()
153
0
  {
154
0
    return _lock;
155
0
  }
156
157
private:
158
  void
159
  _unlock() TS_NO_THREAD_SAFETY_ANALYSIS
160
0
  {
161
0
    int error = pthread_rwlock_unlock(&_lock);
162
0
    if (error != 0) {
163
0
      _call_fatal("pthread_rwlock_unlock", &_lock, error);
164
0
    }
165
0
  }
166
167
#if defined(__linux__)
168
  // Use the initializer that prevents writer starvation.
169
  //
170
  pthread_rwlock_t _lock = PTHREAD_RWLOCK_WRITER_NONRECURSIVE_INITIALIZER_NP;
171
#else
172
  // Testing indicates that for MacOS/Darwin and FreeBSD, pthread rwlocks always prevent writer starvation.
173
  //
174
  pthread_rwlock_t _lock = PTHREAD_RWLOCK_INITIALIZER;
175
#endif
176
177
  [[noreturn]] static void
178
  _call_fatal(char const *func_name, void *ptr, int errnum) noexcept
179
0
  try {
180
0
    fatal_error("{}({}) failed: {} ({})", func_name, ptr, Strerror(errnum).c_str(), errnum);
181
0
  } catch (...) {
182
    // Formatting the message can throw (it allocates); abort with a minimal
183
    // breadcrumb rather than let an exception escape into a noexcept destructor.
184
0
    fprintf(stderr, "shared_mutex: %s failed (%d); abort message formatting threw\n", func_name, errnum);
185
0
    std::abort();
186
0
  }
187
188
  // In debug builds, make sure shared vs. exclusive locks and unlocks are properly paired.
189
  //
190
  X(std::atomic<bool> _exclusive{false};)
191
  X(std::atomic<int> _shared{0};)
192
};
193
194
// RAII guards for ts::shared_mutex that carry Clang thread-safety capability
195
// state. Prefer these over std::unique_lock / std::shared_lock in code annotated
196
// for -Wthread-safety: the analysis does not reliably track the std wrappers
197
// (see tsutil/ts_thread_safety.h).
198
//
199
class TS_SCOPED_CAPABILITY write_guard
200
{
201
public:
202
0
  explicit write_guard(shared_mutex &m) TS_ACQUIRE(m) : _m(m) { _m.lock(); }
203
0
  ~write_guard() TS_RELEASE() { _m.unlock(); }
204
205
  write_guard(write_guard const &)            = delete;
206
  write_guard &operator=(write_guard const &) = delete;
207
208
private:
209
  shared_mutex &_m;
210
};
211
212
class TS_SCOPED_CAPABILITY read_guard
213
{
214
public:
215
0
  explicit read_guard(shared_mutex &m) TS_ACQUIRE_SHARED(m) : _m(m) { _m.lock_shared(); }
216
  // A scoped-capability destructor uses the plain release form even for a shared acquire.
217
0
  ~read_guard() TS_RELEASE() { _m.unlock_shared(); }
218
219
  read_guard(read_guard const &)            = delete;
220
  read_guard &operator=(read_guard const &) = delete;
221
222
private:
223
  shared_mutex &_m;
224
};
225
226
} // end namespace ts
227
228
#undef X