Skip to content

Commit 7065fd8

Browse files
apply change request
1 parent 8b0fc4b commit 7065fd8

File tree

1 file changed

+130
-2
lines changed

1 file changed

+130
-2
lines changed

src/api/context/context.zig

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@ const std = @import("std");
1818
const attributes = @import("../../attributes.zig");
1919
const AttributeValue = attributes.AttributeValue;
2020

21-
/// Thread-safe compile-time key ID generator.
21+
/// Compile-time key ID generator for creating unique IDs during compilation.
2222
///
2323
/// This structure encapsulates the compile-time counter state to prevent
2424
/// type resolution cascades that can occur with bare global variables.
2525
/// Each call to `next()` returns a unique ID starting from 0.
26+
///
27+
/// Note: This is NOT thread-safe at runtime. It only works at compile-time
28+
/// where there is no concurrency. This generator is only called from the
29+
/// `Key()` function which executes at compile-time.
2630
const ComptimeKeyGenerator = struct {
2731
var next_id: usize = 0;
2832

@@ -407,7 +411,7 @@ test "detach error handling" {
407411
try std.testing.expect(try detachContext(token1));
408412
}
409413

410-
test "compile time key creation" {
414+
test "key creation" {
411415
const MyKey = Key("my_service.request_id");
412416
const OtherKey = Key("my_service.request_id");
413417
try std.testing.expect(MyKey.id != OtherKey.id);
@@ -428,3 +432,127 @@ test "context chaining" {
428432
try std.testing.expectEqualStrings("value1", ctx1.getValue(key1).?.string);
429433
try std.testing.expect(ctx1.getValue(key2) == null);
430434
}
435+
436+
test "context thread isolation" {
437+
// Verify that each thread has its own independent context stack
438+
var thread_count = std.atomic.Value(u32).init(0);
439+
440+
const threadWorker = struct {
441+
fn run(counter: *std.atomic.Value(u32)) void {
442+
defer cleanup();
443+
444+
// Each thread should start with an uninitialized context stack
445+
if (context_stack == null) {
446+
_ = counter.fetchAdd(1, .seq_cst);
447+
}
448+
}
449+
}.run;
450+
451+
// Spawn multiple threads to verify isolation
452+
const t1 = try std.Thread.spawn(.{}, threadWorker, .{&thread_count});
453+
const t2 = try std.Thread.spawn(.{}, threadWorker, .{&thread_count});
454+
const t3 = try std.Thread.spawn(.{}, threadWorker, .{&thread_count});
455+
456+
t1.join();
457+
t2.join();
458+
t3.join();
459+
460+
// All threads should have seen null context_stack initially
461+
try std.testing.expectEqual(@as(u32, 3), thread_count.load(.seq_cst));
462+
}
463+
464+
test "context thread-local storage verification" {
465+
// Verify that thread-local storage works correctly for context stacks
466+
var success = std.atomic.Value(bool).init(false);
467+
468+
const verifyThreadLocal = struct {
469+
fn run(result: *std.atomic.Value(bool)) void {
470+
// Verify this thread has its own context_stack variable
471+
if (context_stack == null) {
472+
result.store(true, .seq_cst);
473+
}
474+
}
475+
}.run;
476+
477+
const thread = try std.Thread.spawn(.{}, verifyThreadLocal, .{&success});
478+
thread.join();
479+
480+
try std.testing.expect(success.load(.seq_cst));
481+
}
482+
483+
test "runtime key creation thread safety" {
484+
// This test verifies that createKey() is thread-safe by having multiple
485+
// threads create keys simultaneously and checking for uniqueness
486+
487+
const num_threads = 4;
488+
const keys_per_thread = 100;
489+
490+
// Shared state for collecting results
491+
const SharedData = struct {
492+
keys: std.ArrayList(ContextKey),
493+
mutex: std.Thread.Mutex = .{},
494+
barrier: std.Thread.ResetEvent = .{},
495+
};
496+
497+
var shared = SharedData{
498+
.keys = std.ArrayList(ContextKey).init(std.testing.allocator),
499+
};
500+
defer shared.keys.deinit();
501+
502+
const keyGenWorker = struct {
503+
fn run(data: *SharedData, thread_id: u32) void {
504+
// Wait for all threads to start
505+
data.barrier.wait();
506+
507+
// Generate keys rapidly to stress test atomicity
508+
var local_keys: [keys_per_thread]ContextKey = undefined;
509+
for (0..keys_per_thread) |i| {
510+
var name_buf: [64]u8 = undefined;
511+
const name = std.fmt.bufPrint(
512+
&name_buf,
513+
"thread_{}_key_{}",
514+
.{ thread_id, i },
515+
) catch unreachable;
516+
local_keys[i] = createKey(name);
517+
}
518+
519+
// Add to shared collection
520+
data.mutex.lock();
521+
defer data.mutex.unlock();
522+
data.keys.appendSlice(&local_keys) catch unreachable;
523+
}
524+
}.run;
525+
526+
// Spawn threads
527+
var threads: [num_threads]std.Thread = undefined;
528+
for (0..num_threads) |i| {
529+
threads[i] = try std.Thread.spawn(
530+
.{},
531+
keyGenWorker,
532+
.{ &shared, @as(u32, @intCast(i)) },
533+
);
534+
}
535+
536+
// Start all threads simultaneously
537+
shared.barrier.set();
538+
539+
// Wait for completion
540+
for (threads) |thread| {
541+
thread.join();
542+
}
543+
544+
// Verify we have the expected number of keys
545+
try std.testing.expectEqual(
546+
@as(usize, num_threads * keys_per_thread),
547+
shared.keys.items.len,
548+
);
549+
550+
// Verify all key IDs are unique
551+
var seen = std.AutoHashMap(usize, void).init(std.testing.allocator);
552+
defer seen.deinit();
553+
554+
for (shared.keys.items) |key| {
555+
const result = try seen.getOrPut(key.id);
556+
try std.testing.expect(!result.found_existing);
557+
}
558+
}

0 commit comments

Comments
 (0)