• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LLNL / dftracer / 40940235178

29 Oct 2026 07:27PM UTC coverage: 27.181%. First build
40940235178

Pull #402

github

web-flow
Merge e0fc27880 into de73d284b
Pull Request #402: feat(service): add producer/consumer tracing through dftracer_service

16914 of 83793 branches covered (20.19%)

Branch coverage included in aggregate %.

2684 of 2985 new or added lines in 40 files covered. (89.92%)

11495 of 20726 relevant lines covered (55.46%)

88527.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

33.04
/test/unit/test_ring.cpp
1
// Correctness of the shared-memory event ring (core/ipc): block claiming,
2
// reservation and commit inside a private block, publishing with padding,
3
// flush of a quiet producer's block, full-ring backpressure, and exactly-once,
4
// per-producer-ordered delivery under many producer threads and processes.
5

6
#include <dftracer/core/ipc/ring_producer.h>
7
#include <dftracer/core/ipc/shm_region.h>
8
#include <dftracer/service/pipeline/block_tracker.h>
9
#include <dftracer/service/pipeline/consumer_scaler.h>
10
#include <sys/wait.h>
11
#include <unistd.h>
12

13
#include <atomic>
14
#include <cstring>
15
#include <random>
16
#include <string>
17
#include <thread>
18
#include <vector>
19

20
#include "check.h"
21

22
using namespace dftracer::ipc;
23

24
namespace {
25

26
// DFT_CHECK exits without running destructors, which would leave the
27
// segments of a failed run in /dev/shm. Remove them at exit instead; forked
28
// producers leave through _exit and so never remove the parent's segments.
29
std::vector<std::string>& created_segments() {
11 ✔
30
  static std::vector<std::string> names;
11 !
31
  return names;
11 ✔
32
}
33

34
void remove_created_segments() {
1 ✔
35
  for (const auto& name : created_segments())
10 ✔
36
    boost::interprocess::shared_memory_object::remove(name.c_str());
9 ✔
37
}
1 ✔
38

39
std::string segment_name(const char* what) {
9 ✔
40
  // Construct the list before registering the handler: exit-time cleanup runs
41
  // in reverse order, so the list must outlive the handler that reads it.
42
  static bool registered =
43
      (created_segments(), std::atexit(remove_created_segments), true);
9 !
44
  (void)registered;
45
  std::string name =
46
      "/dftracer-test-ring-" + std::to_string(getpid()) + "-" + what;
18 ✔
47
  created_segments().push_back(name);
9 ✔
48
  return name;
9 ✔
NEW
49
}
×
50

51
// Payload layout written by producers and verified by the consumer.
52
struct TestPayload {
53
  uint32_t producer;
54
  uint32_t bytes;  // total payload bytes, this struct included
55
  uint64_t seq;
56
};
57

58
uint8_t fill_byte(uint32_t producer, uint64_t seq, size_t i) {
245,989,105 ✔
59
  return static_cast<uint8_t>(seq * 131 + producer * 17 + i);
245,989,105 ✔
60
}
61

62
void produce(const RingView& ring, uint32_t producer, uint64_t count,
16 ✔
63
             uint32_t max_bytes, uint32_t seed) {
64
  RingProducer writer(ring, producer + 1);
16 ✔
65
  std::mt19937 rng(seed);
16 ✔
66
  std::uniform_int_distribution<uint32_t> size(sizeof(TestPayload), max_bytes);
16 ✔
67
  for (uint64_t seq = 0; seq < count; ++seq) {
320,016 ✔
68
    const uint32_t bytes = size(rng);
320,000 ✔
69
    Reservation r;
320,000 ✔
70
    RingProducer::Result result;
71
    while ((result = writer.reserve(bytes, SHM_KIND_RAW, r, 1000000000ULL)) ==
320,000 !
72
           RingProducer::Result::FULL) {
73
    }
74
    DFT_CHECK(result == RingProducer::Result::OK);
320,000 !
75
    TestPayload p{producer, bytes, seq};
320,000 ✔
76
    std::memcpy(r.payload, &p, sizeof(p));
320,000 ✔
77
    for (size_t i = sizeof(p); i < bytes; ++i)
93,645,043 ✔
78
      r.payload[i] = fill_byte(producer, seq, i);
93,325,043 ✔
79
    writer.commit(r);
320,000 ✔
80
  }
81
  // The destructor publishes the last, partly filled block.
82
}
16 ✔
83

84
// Drains until every producer's `count` records have been seen, checking each
85
// one. Returns the number of records received.
86
uint64_t consume_and_verify(const RingView& ring, uint32_t producers,
2 ✔
87
                            uint64_t count) {
88
  BlockTracker tracker(ring, /*verify=*/true);
2 ✔
89
  std::vector<uint64_t> next(producers, 0);
2 ✔
90
  uint64_t received = 0;
2 ✔
91
  const uint64_t expected = static_cast<uint64_t>(producers) * count;
2 ✔
92
  while (received < expected) {
37,320 ✔
93
    uint64_t seq;
94
    if (!tracker.poll(seq)) {
37,318 ✔
95
      if (tracker.corrupt() != 0) {
6,301 !
NEW
96
        const uint64_t w = tracker.bad_word();
×
NEW
97
        std::cerr << "corrupt header: seq=" << tracker.bad_seq()
×
NEW
98
                  << " epoch=" << ring.epoch(tracker.bad_seq())
×
NEW
99
                  << " offset=" << tracker.bad_offset()
×
NEW
100
                  << " word.epoch=" << record_epoch(w)
×
NEW
101
                  << " word.length=" << record_length(w) << " word.state=0x"
×
NEW
102
                  << std::hex << int(record_state(w)) << std::dec
×
NEW
103
                  << " head=" << ring.header()->head.load()
×
NEW
104
                  << " armed=" << ring.block(tracker.bad_seq()).armed.load()
×
NEW
105
                  << std::endl;
×
106
      }
107
      DFT_CHECK(tracker.corrupt() == 0);
6,301 !
108
      cpu_relax();
6,301 ✔
109
      continue;
6,301 ✔
110
    }
6,301 ✔
111
    for_each_record(
31,017 ✔
112
        ring, seq,
113
        [&](const RecordHeader& hdr, const uint8_t* payload, uint32_t room) {
240,000 ✔
114
          DFT_CHECK(hdr.kind == SHM_KIND_RAW);
240,000 !
115
          TestPayload p;
116
          std::memcpy(&p, payload, sizeof(p));
240,000 ✔
117
          DFT_CHECK(p.producer < producers);
240,000 !
118
          DFT_CHECK(hdr.pid == p.producer + 1);
240,000 !
119
          DFT_CHECK(p.bytes <= room && room < p.bytes + DFT_SHM_RECORD_ALIGN);
240,000 !
120
          // Exactly once and in the producer's own order.
121
          DFT_CHECK(p.seq == next[p.producer]);
240,000 !
122
          next[p.producer]++;
240,000 ✔
123
          for (size_t i = sizeof(p); i < p.bytes; ++i)
106,279,191 ✔
124
            DFT_CHECK(payload[i] == fill_byte(p.producer, p.seq, i));
106,039,191 !
125
          ++received;
240,000 ✔
126
        });
240,000 ✔
127
    tracker.release(seq);
31,017 ✔
128
  }
129
  for (uint32_t i = 0; i < producers; ++i) DFT_CHECK(next[i] == count);
18 !
130
  return received;
2 ✔
131
}
2 ✔
132

133
void test_layout_and_open() {
1 ✔
134
  DFT_CHECK(RingView::valid_geometry(4, 4096));
1 !
135
  DFT_CHECK(!RingView::valid_geometry(3, 4096));
1 !
136
  DFT_CHECK(!RingView::valid_geometry(4, 1000));
1 !
137
  DFT_CHECK(RingView::data_offset(4) % DFT_SHM_DATA_ALIGN == 0);
1 !
138

139
  const std::string name = segment_name("open");
1 ✔
140
  auto owner = ShmRegion::create(name, 8, 4096);
1 ✔
141
  DFT_CHECK(owner != nullptr);
1 !
142
  DFT_CHECK(owner->ring().header()->magic == DFT_SHM_SEGMENT_MAGIC);
1 !
143
  auto client = ShmRegion::open(name);
1 ✔
144
  DFT_CHECK(client != nullptr);
1 !
145
  DFT_CHECK(client->ring().block_count() == 8);
1 !
146
  DFT_CHECK(client->ring().block_size() == 4096);
1 !
147

148
  // A segment from another ABI is refused rather than misread.
149
  owner->ring().header()->abi_version = DFT_SHM_ABI_VERSION + 1;
1 ✔
150
  DFT_CHECK(ShmRegion::open(name) == nullptr);
1 !
151
  owner->ring().header()->abi_version = DFT_SHM_ABI_VERSION;
1 ✔
152

153
  DFT_CHECK(ShmRegion::open(segment_name("missing")) == nullptr);
1 !
154
}
1 ✔
155

156
void test_single_record_and_flush() {
1 ✔
157
  auto region = ShmRegion::create(segment_name("single"), 4, 4096);
1 ✔
158
  const RingView& ring = region->ring();
1 ✔
159
  RingProducer writer(ring, 42);
1 ✔
160
  BlockTracker tracker(ring, /*verify=*/true);
1 ✔
161

162
  // Nothing is claimed until there is something to write.
163
  writer.flush();
1 ✔
164
  DFT_CHECK(!writer.holding());
1 !
165
  DFT_CHECK(ring.header()->head.load() == 0);
2 !
166

167
  Reservation r;
1 ✔
168
  DFT_CHECK(writer.try_reserve(5, SHM_KIND_RAW, r) == RingProducer::Result::OK);
1 !
169
  DFT_CHECK(r.length == 32);  // 16-byte header + 5 bytes, rounded to 16
1 !
170
  // The first record claims block 0.
171
  DFT_CHECK(writer.holding());
1 !
172
  DFT_CHECK(ring.header()->head.load() == 1);
2 !
173
  DFT_CHECK(ring.block(0).claim_ns.load() != 0);
2 !
174
  std::memcpy(r.payload, "hello", 5);
1 ✔
175

176
  uint64_t seq;
177
  DFT_CHECK(!tracker.poll(seq));
1 !
178
  writer.commit(r);
1 ✔
179
  // Committed, but the block is still private to the producer.
180
  DFT_CHECK(!tracker.poll(seq));
1 !
181
  // Not old enough yet; then old enough.
182
  DFT_CHECK(!writer.flush_if_older(monotonic_ns(), 60000000000ULL));
1 !
183
  DFT_CHECK(writer.flush_if_older(monotonic_ns(), 0));
1 !
184
  DFT_CHECK(!writer.holding());
1 !
185
  DFT_CHECK(tracker.poll(seq));
1 !
186
  DFT_CHECK(seq == 0);
1 !
187
  int seen = 0;
1 ✔
188
  for_each_record(ring, seq,
1 ✔
189
                  [&](const RecordHeader& hdr, const uint8_t* p, uint32_t) {
1 ✔
190
                    DFT_CHECK(hdr.pid == 42);
1 !
191
                    DFT_CHECK(std::memcmp(p, "hello", 5) == 0);
1 !
192
                    ++seen;
1 ✔
193
                  });
1 ✔
194
  DFT_CHECK(seen == 1);
1 !
195
  tracker.release(seq);
1 ✔
196
  // Released block 0 is re-armed for its second lap.
197
  DFT_CHECK(ring.epoch(0) == 1 && ring.epoch(ring.block_count()) == 2);
1 !
198
  DFT_CHECK(ring.block(0).armed.load() == 2);
2 !
199
  DFT_CHECK(ring.block(0).claim_ns.load() == 0);
2 !
200

201
  // A reservation that is never committed is published as padding.
202
  DFT_CHECK(writer.try_reserve(5, SHM_KIND_RAW, r) == RingProducer::Result::OK);
1 !
203
  writer.flush();
1 ✔
204
  DFT_CHECK(tracker.poll(seq) && seq == 1);
1 !
205
  seen = 0;
1 ✔
206
  for_each_record(
1 ✔
207
      ring, seq,
NEW
208
      [&](const RecordHeader&, const uint8_t*, uint32_t) { ++seen; });
×
209
  DFT_CHECK(seen == 0);
1 !
210
  tracker.release(seq);
1 ✔
211
}
1 ✔
212

213
void test_too_large_and_exact_fit() {
1 ✔
214
  auto region = ShmRegion::create(segment_name("large"), 4, 4096);
1 ✔
215
  const RingView& ring = region->ring();
1 ✔
216
  RingProducer writer(ring, 1);
1 ✔
217
  Reservation r;
1 ✔
218
  DFT_CHECK(writer.try_reserve(4096, SHM_KIND_RAW, r) ==
1 !
219
            RingProducer::Result::TOO_LARGE);
220
  DFT_CHECK(!writer.holding());
1 !
221
  // A record exactly one block long fits.
222
  DFT_CHECK(writer.try_reserve(4096 - sizeof(RecordHeader), SHM_KIND_RAW, r) ==
1 !
223
            RingProducer::Result::OK);
224
  writer.commit(r);
1 ✔
225
  BlockTracker tracker(ring, /*verify=*/true);
1 ✔
226
  uint64_t seq;
227
  DFT_CHECK(!tracker.poll(seq));
1 !
228
  // The next reservation publishes block 0 and claims block 1.
229
  DFT_CHECK(writer.try_reserve(8, SHM_KIND_RAW, r) == RingProducer::Result::OK);
1 !
230
  writer.commit(r);
1 ✔
231
  DFT_CHECK(ring.header()->head.load() == 2);
2 !
232
  DFT_CHECK(tracker.poll(seq) && seq == 0);
1 !
233
}
1 ✔
234

235
void test_full_ring() {
1 ✔
236
  auto region = ShmRegion::create(segment_name("full"), 2, 4096);
1 ✔
237
  const RingView& ring = region->ring();
1 ✔
238
  RingProducer writer(ring, 1);
1 ✔
239
  BlockTracker tracker(ring, /*verify=*/true);
1 ✔
240
  Reservation r;
1 ✔
241
  uint64_t accepted = 0;
1 ✔
242
  RingProducer::Result result;
243
  while ((result = writer.try_reserve(1000, SHM_KIND_RAW, r)) ==
9 ✔
244
         RingProducer::Result::OK) {
245
    writer.commit(r);
8 ✔
246
    ++accepted;
8 ✔
247
    DFT_CHECK(accepted < 100);
8 !
248
  }
249
  DFT_CHECK(result == RingProducer::Result::FULL);
1 !
250
  // 1000 bytes + 16-byte header round to 1024, so four records fill each
251
  // 4 KiB block exactly: eight in all before the ring is full. The failed
252
  // attempt published the second block before trying to claim a third.
253
  DFT_CHECK(accepted == 8);
1 !
254
  DFT_CHECK(!writer.holding());
1 !
255
  // Repeated attempts on a full ring claim nothing.
256
  for (int i = 0; i < 1000; ++i)
1,001 ✔
257
    DFT_CHECK(writer.try_reserve(1000, SHM_KIND_RAW, r) ==
1,000 !
258
              RingProducer::Result::FULL);
259
  DFT_CHECK(ring.header()->head.load() == 2);
2 !
260
  // A bounded wait gives up.
261
  DFT_CHECK(writer.reserve(1000, SHM_KIND_RAW, r, 1000000) ==
1 !
262
            RingProducer::Result::FULL);
263

264
  uint64_t seq;
265
  DFT_CHECK(tracker.poll(seq) && seq == 0);
1 !
266
  tracker.release(seq);
1 ✔
267
  DFT_CHECK(tracker.poll(seq) && seq == 1);
1 !
268
  DFT_CHECK(writer.try_reserve(1000, SHM_KIND_RAW, r) ==
1 !
269
            RingProducer::Result::OK);
270
  writer.commit(r);
1 ✔
271
  DFT_CHECK(ring.header()->head.load() == 3);
2 !
272
}
1 ✔
273

274
void test_many_threads() {
1 ✔
275
  auto region = ShmRegion::create(segment_name("threads"), 16, 4096);
1 ✔
276
  const RingView& ring = region->ring();
1 ✔
277
  const uint32_t producers = 8;
1 ✔
278
  const uint64_t count = 20000;
1 ✔
279
  std::vector<std::thread> threads;
1 ✔
280
  for (uint32_t p = 0; p < producers; ++p)
9 ✔
281
    threads.emplace_back(produce, ring, p, count, 600, 1234 + p);
8 ✔
282
  DFT_CHECK(consume_and_verify(ring, producers, count) == producers * count);
1 !
283
  for (auto& t : threads) t.join();
9 ✔
284
}
1 ✔
285

286
// Several consumer threads take blocks through ParallelBlockClaimer while
287
// several producers fill them. Blocks are processed and released out of
288
// order, so per-producer order only holds within a block: check every record
289
// arrives exactly once, intact.
290
void test_parallel_consumers() {
1 ✔
291
  auto region = ShmRegion::create(segment_name("pconsume"), 16, 4096);
1 ✔
292
  const RingView& ring = region->ring();
1 ✔
293
  const uint32_t producers = 8;
1 ✔
294
  const uint32_t consumers = 4;
1 ✔
295
  const uint64_t count = 20000;
1 ✔
296
  std::vector<std::atomic<uint8_t>> seen(producers * count);
1 ✔
297
  for (auto& s : seen) s.store(0);
160,001 ✔
298
  std::atomic<uint64_t> received{0};
1 ✔
299
  ParallelBlockClaimer claimer(ring);
1 ✔
300
  std::vector<std::thread> threads;
1 ✔
301
  for (uint32_t p = 0; p < producers; ++p)
9 ✔
302
    threads.emplace_back(produce, ring, p, count, 600, 777 + p);
8 ✔
303
  for (uint32_t c = 0; c < consumers; ++c) {
5 ✔
304
    threads.emplace_back([&]() {
4 ✔
305
      while (received.load() < producers * count) {
13,124,694 ✔
306
        uint64_t seq;
307
        if (!claimer.claim(seq)) {
6,562,343 ✔
308
          cpu_relax();
6,548,735 ✔
309
          continue;
6,548,735 ✔
310
        }
NEW
311
        for_each_record(
×
312
            ring, seq,
13,608 ✔
313
            [&](const RecordHeader& hdr, const uint8_t* payload,
160,000 ✔
314
                uint32_t room) {
315
              TestPayload p;
316
              std::memcpy(&p, payload, sizeof(p));
160,000 ✔
317
              DFT_CHECK(p.producer < producers && p.seq < count);
160,000 !
318
              DFT_CHECK(hdr.pid == p.producer + 1);
160,000 !
319
              DFT_CHECK(p.bytes <= room &&
160,000 !
320
                        room < p.bytes + DFT_SHM_RECORD_ALIGN);
321
              for (size_t i = sizeof(p); i < p.bytes; ++i)
46,784,871 ✔
322
                DFT_CHECK(payload[i] == fill_byte(p.producer, p.seq, i));
46,624,871 !
323
              DFT_CHECK(seen[p.producer * count + p.seq].exchange(1) == 0);
333,608 !
324
              received.fetch_add(1);
160,000 ✔
325
            });
160,000 ✔
326
        claimer.release(seq);
13,608 ✔
327
      }
328
    });
4 ✔
329
  }
330
  for (auto& t : threads) t.join();
13 ✔
331
  DFT_CHECK(received.load() == producers * count);
1 !
332
  for (auto& s : seen) DFT_CHECK(s.load() == 1);
160,001 !
333
  // Every block was released, out of order or not, so the tail caught up.
334
  if (claimer.backlog_blocks() != 0)
1 !
NEW
335
    std::cerr << "head=" << ring.header()->head.load()
×
NEW
336
              << " next=" << claimer.next() << " tail=" << claimer.tail()
×
NEW
337
              << std::endl;
×
338
  DFT_CHECK(claimer.backlog_blocks() == 0);
1 !
339
}
1 ✔
340

341
void test_many_processes() {
1 ✔
342
  const std::string name = segment_name("procs");
1 ✔
343
  auto region = ShmRegion::create(name, 16, 4096);
1 ✔
344
  const uint32_t processes = 4;
1 ✔
345
  const uint32_t threads_each = 2;
1 ✔
346
  const uint64_t count = 10000;
1 ✔
347
  std::vector<pid_t> children;
1 ✔
348
  for (uint32_t c = 0; c < processes; ++c) {
5 ✔
349
    pid_t pid = fork();
4 ✔
350
    DFT_CHECK(pid >= 0);
4 !
351
    if (pid == 0) {
4 !
352
      // Attach by name, as a traced process would, not through the mapping
353
      // inherited from the parent.
NEW
354
      auto client = ShmRegion::open(name);
×
NEW
355
      if (!client) _exit(2);
×
NEW
356
      std::vector<std::thread> threads;
×
NEW
357
      for (uint32_t t = 0; t < threads_each; ++t) {
×
NEW
358
        uint32_t producer = c * threads_each + t;
×
NEW
359
        threads.emplace_back(produce, client->ring(), producer, count, 1500,
×
NEW
360
                             99 + producer);
×
361
      }
NEW
362
      for (auto& t : threads) t.join();
×
NEW
363
      _exit(0);
×
NEW
364
    }
×
365
    children.push_back(pid);
4 ✔
366
  }
367
  DFT_CHECK(consume_and_verify(region->ring(), processes * threads_each,
1 !
368
                               count) == processes * threads_each * count);
369
  for (pid_t pid : children) {
5 ✔
370
    int status = 0;
4 ✔
371
    DFT_CHECK(waitpid(pid, &status, 0) == pid);
4 !
372
    DFT_CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
4 !
373
  }
374
}
1 ✔
375

376
}  // namespace
377

378
// Thread-count controller: up at once to cover arrivals plus backlog, down one
379
// thread per `down_ticks` quiet ticks, never outside [min, max].
380
void test_consumer_scaler() {
1 ✔
381
  const uint64_t ms = 1000000;
1 ✔
382
  ConsumerScaler scaler(1, 8, 100 * ms, 3, 1.0);
1 ✔
383
  ConsumerScaler::Sample s{};
1 ✔
384
  uint32_t active = 1;
1 ✔
385
  active = scaler.update(s, active);  // baseline only
1 ✔
386
  DFT_CHECK(active == 1);
1 !
387

388
  // A thread drains 1 block per ms of busy time. 4 blocks/ms arrive: 4 threads.
389
  auto tick = [&](uint64_t arrived, uint64_t drained, uint64_t ready) {
41 ✔
390
    s.now_ns += 10 * ms;
41 ✔
391
    s.head += arrived;
41 ✔
392
    s.drained += drained;
41 ✔
393
    s.busy_ns += drained * ms;
41 ✔
394
    s.ready = ready;
41 ✔
395
    active = scaler.update(s, active);
41 ✔
396
  };
42 ✔
397
  tick(40, 10, 0);
1 ✔
398
  DFT_CHECK(active == 4);
1 !
399
  // Same rate, plus 100 blocks behind: 1 more block/ms to clear in 100 ms.
400
  tick(40, 40, 100);
1 ✔
401
  DFT_CHECK(active == 5);
1 !
402
  // Never above max.
403
  tick(400, 50, 1000);
1 ✔
404
  DFT_CHECK(active == 8);
1 !
405
  // Load gone: hold for down_ticks - 1 ticks, then one thread per 3 ticks.
406
  tick(0, 10, 0);
1 ✔
407
  tick(0, 10, 0);
1 ✔
408
  DFT_CHECK(active == 8);
1 !
409
  tick(0, 10, 0);
1 ✔
410
  DFT_CHECK(active == 7);
1 !
411
  for (int i = 0; i < 3 * 10; ++i) tick(0, 10, 0);
31 ✔
412
  DFT_CHECK(active == 1);
1 !
413
  // A burst between quiet ticks resets the step-down count.
414
  tick(0, 10, 0);
1 ✔
415
  tick(0, 10, 0);
1 ✔
416
  tick(20, 10, 0);
1 ✔
417
  DFT_CHECK(active == 2);
1 !
418
  tick(0, 10, 0);
1 ✔
419
  tick(0, 10, 0);
1 ✔
420
  DFT_CHECK(active == 2);
1 !
421

422
  // Before any block has been drained there is no rate: one thread at a time.
423
  ConsumerScaler cold(2, 8, 100 * ms, 3);
1 ✔
424
  ConsumerScaler::Sample c{};
1 ✔
425
  uint32_t n = cold.update(c, 1);
1 ✔
426
  DFT_CHECK(n == 2);  // raised to min
1 !
427
  c.now_ns += 10 * ms;
1 ✔
428
  c.head = 50;
1 ✔
429
  c.ready = 50;
1 ✔
430
  n = cold.update(c, n);
1 ✔
431
  DFT_CHECK(n == 3);
1 !
432
}
1 ✔
433

434
// Write one committed record of `value` through `writer`.
435
void put(RingProducer& writer, uint32_t value) {
6 ✔
436
  Reservation r;
6 ✔
437
  DFT_CHECK(writer.try_reserve(sizeof(value), SHM_KIND_RAW, r) ==
6 !
438
            RingProducer::Result::OK);
439
  std::memcpy(r.payload, &value, sizeof(value));
6 ✔
440
  writer.commit(r);
6 ✔
441
}
6 ✔
442

443
// Records of block `seq` as values, walked as the service walks it.
444
std::vector<uint32_t> values_of(const RingView& ring, uint64_t seq,
4 ✔
445
                                bool salvage, uint32_t* uncommitted = nullptr) {
446
  std::vector<uint32_t> values;
4 ✔
447
  uint32_t n = for_each_record(
4 ✔
448
      ring, seq,
449
      [&](const RecordHeader&, const uint8_t* payload, uint32_t) {
7 ✔
450
        uint32_t v;
451
        std::memcpy(&v, payload, sizeof(v));
7 ✔
452
        values.push_back(v);
7 ✔
453
      },
7 ✔
454
      salvage);
455
  if (uncommitted != nullptr) *uncommitted = n;
4 ✔
456
  return values;
4 ✔
NEW
457
}
×
458

459
// A block its producer keeps without publishing neither holds back the blocks
460
// after it nor stays stuck: the service passes over it, then takes it over.
461
void test_take_over() {
1 ✔
462
  auto region = ShmRegion::create(segment_name("takeover"), 8, 4096);
1 ✔
463
  const RingView& ring = region->ring();
1 ✔
464
  const uint32_t pid = static_cast<uint32_t>(getpid());
1 ✔
465
  ParallelBlockClaimer claimer(ring);
1 ✔
466
  uint64_t seq;
467
  uint32_t owner;
468

469
  RingProducer quiet(ring, pid, 11);
1 ✔
470
  for (uint32_t v = 1; v <= 3; ++v) put(quiet, v);  // block 0, held
4 ✔
471
  RingProducer busy(ring, pid, 12);
1 ✔
472
  put(busy, 100);
1 ✔
473
  busy.flush();  // block 1, published
1 ✔
474
  DFT_CHECK(claimer.claim(seq) && seq == 1);
1 !
475
  DFT_CHECK(ring.block(seq).owner_tid.load() == 12);
2 !
476
  DFT_CHECK(values_of(ring, seq, false) == std::vector<uint32_t>{100});
1 !
477
  claimer.release(seq);
1 ✔
478
  DFT_CHECK(!claimer.claim(seq));
1 !
479

480
  // Too young to take; then taken, with every committed record.
481
  const uint64_t claimed = ring.block(0).claim_ns.load();
1 ✔
482
  DFT_CHECK(!claimer.take_over(seq, claimed - 1, owner));
1 !
483
  DFT_CHECK(claimer.take_over(seq, monotonic_ns(), owner));
1 !
484
  DFT_CHECK(seq == 0 && owner == pid);
1 !
485
  DFT_CHECK(ring.block(seq).owner_tid.load() == 11);
2 !
486
  uint32_t uncommitted = 1;
1 ✔
487
  DFT_CHECK((values_of(ring, seq, true, &uncommitted) ==
1 !
488
             std::vector<uint32_t>{1, 2, 3}));
489
  DFT_CHECK(uncommitted == 0);
1 !
490
  claimer.release(seq);
1 ✔
491
  DFT_CHECK(!claimer.take_over(seq, monotonic_ns(), owner));
1 !
492

493
  // The producer notices, moves to a new block, and loses nothing after.
494
  put(quiet, 4);
1 ✔
495
  DFT_CHECK(quiet.taken_blocks() == 1);
1 !
496
  quiet.flush();
1 ✔
497
  DFT_CHECK(claimer.claim(seq) && seq == 2);
1 !
498
  DFT_CHECK(values_of(ring, seq, false) == std::vector<uint32_t>{4});
1 !
499
  claimer.release(seq);
1 ✔
500

501
  // A record being written is never taken from a live producer...
502
  RingProducer writing(ring, pid, 13);
1 ✔
503
  put(writing, 5);
1 ✔
504
  Reservation open;
1 ✔
505
  DFT_CHECK(writing.try_reserve(4, SHM_KIND_RAW, open) ==
1 !
506
            RingProducer::Result::OK);
507
  DFT_CHECK(!claimer.take_over(seq, monotonic_ns(), owner));
1 !
508
  writing.commit(open);
1 ✔
509
  writing.flush();
1 ✔
510
  DFT_CHECK(claimer.claim(seq));
1 !
511
  claimer.release(seq);
1 ✔
512

513
  // ...but is from a dead one: committed records kept, the open one skipped.
514
  pid_t child = fork();
1 ✔
515
  if (child == 0) {
1 !
NEW
516
    RingProducer dying(ring, static_cast<uint32_t>(getpid()), 14);
×
NEW
517
    put(dying, 6);
×
NEW
518
    put(dying, 7);
×
NEW
519
    Reservation never;
×
NEW
520
    if (dying.try_reserve(4, SHM_KIND_RAW, never) != RingProducer::Result::OK)
×
NEW
521
      _exit(1);
×
NEW
522
    _exit(0);  // no destructor: the block stays WRITING
×
NEW
523
  }
×
524
  int status = 0;
1 ✔
525
  waitpid(child, &status, 0);
1 ✔
526
  DFT_CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
1 !
527
  DFT_CHECK(claimer.take_over(seq, monotonic_ns(), owner));
1 !
528
  DFT_CHECK(owner == static_cast<uint32_t>(child));
1 !
529
  DFT_CHECK((values_of(ring, seq, true, &uncommitted) ==
1 !
530
             std::vector<uint32_t>{6, 7}));
531
  DFT_CHECK(uncommitted == 1);
1 !
532
  claimer.release(seq);
1 ✔
533
  DFT_CHECK(claimer.backlog_blocks() == 0);
1 !
534
}
1 ✔
535

536
// The service shrinks its ring to the shared memory there is: fewer blocks,
537
// then smaller ones, never below the minimum.
538
void test_fit_geometry() {
1 ✔
539
  const uint64_t mib = 1 << 20;
1 ✔
540
  uint32_t count = 1024, size = 1 << 20;
1 ✔
541
  DFT_CHECK(RingView::fit(count, size, 4096 * mib));  // fits as asked
1 !
542
  DFT_CHECK(count == 1024 && size == (1u << 20));
1 !
543
  DFT_CHECK(RingView::fit(count, size, 60 * mib));  // a 64 MB container
1 !
544
  DFT_CHECK(count == 32 && size == (1u << 20));
1 !
545
  DFT_CHECK(RingView::bytes_for(count, size) <= 60 * mib);
1 !
546
  count = 1024, size = 1 << 20;
1 ✔
547
  DFT_CHECK(RingView::fit(count, size, 2 * mib));  // blocks shrink last
1 !
548
  DFT_CHECK(count == DFT_SHM_MIN_BLOCKS && size == (64u << 10));
1 !
549
  count = 1024, size = 1 << 20;
1 ✔
550
  DFT_CHECK(!RingView::fit(count, size, 64 * 1024));  // nothing fits
1 !
551
  DFT_CHECK(count == DFT_SHM_MIN_BLOCKS && size == DFT_SHM_MIN_BLOCK_SIZE);
1 !
552
  DFT_CHECK(RingView::valid_geometry(count, size));
1 !
553
}
1 ✔
554

555
int main() {
1 ✔
556
  test_layout_and_open();
1 ✔
557
  test_single_record_and_flush();
1 ✔
558
  test_too_large_and_exact_fit();
1 ✔
559
  test_full_ring();
1 ✔
560
  test_many_threads();
1 ✔
561
  test_parallel_consumers();
1 ✔
562
  test_consumer_scaler();
1 ✔
563
  test_take_over();
1 ✔
564
  test_fit_geometry();
1 ✔
565
#if !defined(__SANITIZE_THREAD__)
566
  // ThreadSanitizer does not see synchronization that goes through another
567
  // process (here: producer commit -> consumer release -> producer reuse), so
568
  // it reports reuse of a released block as a race. Covered without TSan.
569
  test_many_processes();
1 ✔
570
#endif
571
  std::cout << "test_ring: all checks passed" << std::endl;
1 ✔
572
  return 0;
1 ✔
573
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc