@@ -43,13 +43,26 @@ CallTraceStorage::CallTraceStorage() : _lock(0) {
43
43
}
44
44
45
45
CallTraceStorage::~CallTraceStorage () {
46
- // Unique pointers will automatically clean up
46
+ TEST_LOG (" CallTraceStorage::~CallTraceStorage() - shutting down, invalidating active storage to prevent use-after-destruction" );
47
+
48
+ // Take exclusive lock to ensure no ongoing put() operations
49
+ _lock.lock ();
50
+
51
+ // Invalidate active storage first to prevent use-after-destruction
52
+ // Any subsequent put() calls will see nullptr and return DROPPED_TRACE_ID safely
53
+ _active_storage = nullptr ;
54
+ _standby_storage = nullptr ;
55
+
56
+ _lock.unlock ();
57
+
58
+ TEST_LOG (" CallTraceStorage::~CallTraceStorage() - destruction complete" );
59
+ // Unique pointers will automatically clean up the actual objects
47
60
}
48
61
49
62
CallTrace* CallTraceStorage::getDroppedTrace () {
50
63
// Static dropped trace object - created once and reused
51
64
// Use same pattern as storage_overflow trace for consistent platform handling
52
- static CallTrace dropped_trace = {false , 1 , DROPPED_TRACE_ID, {BCI_ERROR, LP64_ONLY (0 COMMA) (jmethodID)" <dropped due to contention >" }};
65
+ static CallTrace dropped_trace = {false , 1 , DROPPED_TRACE_ID, {BCI_ERROR, LP64_ONLY (0 COMMA) (jmethodID)" <dropped>" }};
53
66
54
67
return &dropped_trace;
55
68
}
@@ -75,6 +88,14 @@ u64 CallTraceStorage::put(int num_frames, ASGCT_CallFrame* frames, bool truncate
75
88
return DROPPED_TRACE_ID;
76
89
}
77
90
91
+ // Safety check: if active storage is invalid (e.g., during destruction), drop the sample
92
+ if (_active_storage == nullptr ) {
93
+ TEST_LOG (" CallTraceStorage::put() - _active_storage is NULL (shutdown/destruction?), returning DROPPED_TRACE_ID" );
94
+ _lock.unlockShared ();
95
+ Counters::increment (CALLTRACE_STORAGE_DROPPED);
96
+ return DROPPED_TRACE_ID;
97
+ }
98
+
78
99
// Forward to active storage
79
100
u64 result = _active_storage->put (num_frames, frames, truncated, weight);
80
101
@@ -84,7 +105,6 @@ u64 CallTraceStorage::put(int num_frames, ASGCT_CallFrame* frames, bool truncate
84
105
85
106
void CallTraceStorage::processTraces (std::function<void (const std::unordered_set<CallTrace*>&)> processor) {
86
107
// Split lock strategy: minimize time under exclusive lock by separating swap from processing
87
- std::unique_ptr<CallTraceHashTable> old_storage;
88
108
std::unordered_set<u64 > preserve_set;
89
109
90
110
// PHASE 1: Brief exclusive lock for liveness collection and storage swap
@@ -108,13 +128,9 @@ void CallTraceStorage::processTraces(std::function<void(const std::unordered_set
108
128
u64 new_instance_id = getNextInstanceId ();
109
129
_standby_storage->setInstanceId (new_instance_id);
110
130
111
- // Step 3: Swap storage immediately - standby (with new instance ID) becomes active
112
- // Take ownership of old storage for lock-free processing
131
+ // Step 3: Swap storage atomically - standby (with new instance ID) becomes active
132
+ // Old active becomes standby and will be processed lock-free
113
133
_active_storage.swap (_standby_storage);
114
- old_storage = std::move (_standby_storage);
115
-
116
- // Create new standby storage immediately to minimize future swap time
117
- _standby_storage = std::make_unique<CallTraceHashTable>();
118
134
119
135
_lock.unlock ();
120
136
// END PHASE 1 - Lock released, put() operations can now proceed concurrently
@@ -125,7 +141,7 @@ void CallTraceStorage::processTraces(std::function<void(const std::unordered_set
125
141
std::unordered_set<CallTrace*> traces_to_preserve;
126
142
127
143
// Collect all traces and identify which ones to preserve (no lock held)
128
- old_storage ->collect (traces); // Get all traces for JFR processing
144
+ _standby_storage ->collect (traces); // Get all traces from standby (old active) for JFR processing
129
145
130
146
// Always ensure the dropped trace is included in JFR constant pool
131
147
// This guarantees that events with DROPPED_TRACE_ID have a valid stack trace entry
@@ -138,11 +154,11 @@ void CallTraceStorage::processTraces(std::function<void(const std::unordered_set
138
154
}
139
155
}
140
156
141
- // Process traces while they're still valid in old storage (no lock held)
157
+ // Process traces while they're still valid in standby storage (no lock held)
142
158
// The callback is guaranteed that all traces remain valid during execution
143
159
processor (traces);
144
160
145
- // PHASE 3: Brief exclusive lock to copy preserved traces back to active storage
161
+ // PHASE 3: Brief exclusive lock to copy preserved traces back to active storage and clear standby
146
162
{
147
163
_lock.lock ();
148
164
@@ -151,12 +167,13 @@ void CallTraceStorage::processTraces(std::function<void(const std::unordered_set
151
167
_active_storage->putWithExistingId (trace, 1 );
152
168
}
153
169
170
+ // Clear standby storage (old active) now that we're done processing
171
+ // This keeps the hash table structure but clears all data
172
+ _standby_storage->clear ();
173
+
154
174
_lock.unlock ();
155
- // END PHASE 3 - All preserved traces copied back to active storage
175
+ // END PHASE 3 - All preserved traces copied back to active storage, standby cleared for reuse
156
176
}
157
-
158
- // old_storage automatically destroyed when unique_ptr goes out of scope
159
- // No need to explicitly clear - destructor handles cleanup
160
177
}
161
178
162
179
0 commit comments