| 178 | } |
| 179 | |
| 180 | void Blackboard::cloneInto(Blackboard& dst) const |
| 181 | { |
| 182 | // We must never hold storage_mutex_ while locking entry_mutex, because |
| 183 | // the script evaluation path (ExprAssignment::evaluate) does the reverse: |
| 184 | // entry_mutex → storage_mutex_ (via entryInfo → getEntry). |
| 185 | // |
| 186 | // Strategy: collect shared_ptrs under storage_mutex_, release it, |
| 187 | // then copy entry data under entry_mutex only. |
| 188 | |
| 189 | struct CopyTask |
| 190 | { |
| 191 | std::string key; |
| 192 | std::shared_ptr<Entry> src; |
| 193 | std::shared_ptr<Entry> dst; // nullptr when a new entry is needed |
| 194 | }; |
| 195 | |
| 196 | std::vector<CopyTask> tasks; |
| 197 | std::vector<std::string> keys_to_remove; |
| 198 | |
| 199 | // Step 1: snapshot src/dst entries under both storage_mutex_ locks. |
| 200 | { |
| 201 | std::shared_lock lk1(storage_mutex_, std::defer_lock); |
| 202 | std::unique_lock lk2(dst.storage_mutex_, std::defer_lock); |
| 203 | std::lock(lk1, lk2); |
| 204 | |
| 205 | std::unordered_set<std::string> dst_keys; |
| 206 | for(const auto& [key, entry] : dst.storage_) |
| 207 | { |
| 208 | dst_keys.insert(key); |
| 209 | } |
| 210 | |
| 211 | for(const auto& [src_key, src_entry] : storage_) |
| 212 | { |
| 213 | dst_keys.erase(src_key); |
| 214 | auto it = dst.storage_.find(src_key); |
| 215 | if(it != dst.storage_.end()) |
| 216 | { |
| 217 | tasks.push_back({ src_key, src_entry, it->second }); |
| 218 | } |
| 219 | else |
| 220 | { |
| 221 | tasks.push_back({ src_key, src_entry, nullptr }); |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | for(const auto& key : dst_keys) |
| 226 | { |
| 227 | keys_to_remove.push_back(key); |
| 228 | } |
| 229 | } |
| 230 | // storage_mutex_ locks released here |
| 231 | |
| 232 | // Step 2: copy entry data under entry_mutex only (BUG-3 fix). |
| 233 | std::vector<std::pair<std::string, std::shared_ptr<Entry>>> new_entries; |
| 234 | |
| 235 | for(auto& task : tasks) |
| 236 | { |
| 237 | if(task.dst) |