1414
1515
1616#include < algorithm>
17+ #include < atomic>
1718#include < boost/regex.hpp>
19+ #include < chrono>
1820#include < fstream>
1921#include < memory>
2022#include < set>
2123#include < string>
24+ #include < thread>
2225#include < unordered_map>
2326#include < vector>
2427
@@ -54,6 +57,7 @@ class ContainerManagerUnittest : public testing::Test {
5457 void TestContainerMatchingConsistency () const ;
5558 void TestcomputeMatchedContainersDiffWithSnapshot () const ;
5659 void TestNullRawContainerInfoHandling () const ;
60+ void TestConcurrentContainerMapAccess () const ;
5761 void runTestFile (const std::string& testFilePath) const ;
5862
5963private:
@@ -1329,15 +1333,15 @@ void ContainerManagerUnittest::TestcomputeMatchedContainersDiffWithSnapshot() co
13291333 // Should only add running containers to both diff.mAdded and fullList
13301334 EXPECT_EQ (diff.mAdded .size (), 2 );
13311335 EXPECT_EQ (fullList.size (), 2 ); // Only running containers are added to fullList when isStdio=false
1332-
1336+
13331337 std::set<std::string> addedIds;
13341338 for (const auto & container : diff.mAdded ) {
13351339 addedIds.insert (container->mID );
13361340 }
13371341 EXPECT_TRUE (addedIds.find (" snapshot1" ) != addedIds.end ());
13381342 EXPECT_TRUE (addedIds.find (" snapshot2" ) != addedIds.end ());
13391343 EXPECT_TRUE (addedIds.find (" snapshot3" ) == addedIds.end ()); // exited container should not be added
1340-
1344+
13411345 // Verify fullList only contains running containers
13421346 EXPECT_TRUE (fullList.find (" snapshot1" ) != fullList.end ());
13431347 EXPECT_TRUE (fullList.find (" snapshot2" ) != fullList.end ());
@@ -1356,15 +1360,15 @@ void ContainerManagerUnittest::TestcomputeMatchedContainersDiffWithSnapshot() co
13561360 // Should add all containers when isStdio=true (including exited)
13571361 EXPECT_EQ (diff.mAdded .size (), 3 );
13581362 EXPECT_EQ (fullList.size (), 3 ); // All containers are added to fullList when isStdio=true
1359-
1363+
13601364 std::set<std::string> addedIds;
13611365 for (const auto & container : diff.mAdded ) {
13621366 addedIds.insert (container->mID );
13631367 }
13641368 EXPECT_TRUE (addedIds.find (" snapshot1" ) != addedIds.end ());
13651369 EXPECT_TRUE (addedIds.find (" snapshot2" ) != addedIds.end ());
13661370 EXPECT_TRUE (addedIds.find (" snapshot3" ) != addedIds.end ());
1367-
1371+
13681372 // Verify fullList contains all containers
13691373 EXPECT_TRUE (fullList.find (" snapshot1" ) != fullList.end ());
13701374 EXPECT_TRUE (fullList.find (" snapshot2" ) != fullList.end ());
@@ -1376,13 +1380,13 @@ void ContainerManagerUnittest::TestcomputeMatchedContainersDiffWithSnapshot() co
13761380 std::set<std::string> fullList;
13771381 fullList.insert (" snapshot1" );
13781382 fullList.insert (" removed1" ); // This container exists in fullList but not in mContainerMap
1379-
1383+
13801384 std::unordered_map<std::string, std::shared_ptr<RawContainerInfo>> matchList;
13811385 RawContainerInfo removedInfo;
13821386 removedInfo.mID = " removed1" ;
13831387 removedInfo.mLogPath = " /var/lib/docker/containers/removed1/logs" ;
13841388 matchList[" removed1" ] = std::make_shared<RawContainerInfo>(removedInfo);
1385-
1389+
13861390 ContainerFilters filters;
13871391 ContainerDiff diff;
13881392
@@ -1392,19 +1396,19 @@ void ContainerManagerUnittest::TestcomputeMatchedContainersDiffWithSnapshot() co
13921396 EXPECT_TRUE (std::find (diff.mRemoved .begin (), diff.mRemoved .end (), " removed1" ) != diff.mRemoved .end ());
13931397 // The removed container should no longer be in fullList
13941398 EXPECT_EQ (fullList.count (" removed1" ), 0 );
1395-
1399+
13961400 // snapshot1 should still be in fullList
13971401 EXPECT_TRUE (fullList.find (" snapshot1" ) != fullList.end ());
1398-
1402+
13991403 // snapshot2 should be added to fullList (running container not previously in fullList)
14001404 EXPECT_TRUE (fullList.find (" snapshot2" ) != fullList.end ());
1401-
1405+
14021406 // snapshot3 should NOT be in fullList (exited container with isStdio=false)
14031407 EXPECT_TRUE (fullList.find (" snapshot3" ) == fullList.end ());
1404-
1408+
14051409 // Final fullList size should be 2 (snapshot1 and snapshot2)
14061410 EXPECT_EQ (fullList.size (), 2 );
1407-
1411+
14081412 // diff.mAdded should contain snapshot2
14091413 EXPECT_EQ (diff.mAdded .size (), 1 );
14101414 if (diff.mAdded .size () > 0 ) {
@@ -1416,13 +1420,13 @@ void ContainerManagerUnittest::TestcomputeMatchedContainersDiffWithSnapshot() co
14161420 {
14171421 std::set<std::string> fullList;
14181422 fullList.insert (" snapshot1" );
1419-
1423+
14201424 std::unordered_map<std::string, std::shared_ptr<RawContainerInfo>> matchList;
14211425 RawContainerInfo oldInfo;
14221426 oldInfo.mID = " snapshot1" ;
14231427 oldInfo.mLogPath = " /old/path" ; // Different from current mContainerMap
14241428 matchList[" snapshot1" ] = std::make_shared<RawContainerInfo>(oldInfo);
1425-
1429+
14261430 ContainerFilters filters;
14271431 ContainerDiff diff;
14281432
@@ -1440,82 +1444,82 @@ void ContainerManagerUnittest::TestcomputeMatchedContainersDiffWithSnapshot() co
14401444void ContainerManagerUnittest::TestNullRawContainerInfoHandling () const {
14411445 // This test verifies that null mRawContainerInfo pointers are handled gracefully
14421446 // as added in the race condition fix
1443-
1447+
14441448 // Note: This test is limited because ContainerInfo is typically managed internally
14451449 // and we cannot easily inject null mRawContainerInfo in the current implementation.
14461450 // However, we can test the defensive checks are in place by verifying the code
14471451 // doesn't crash with various edge cases.
1448-
1452+
14491453 ContainerManager containerManager;
1450-
1454+
14511455 // Test 1: Empty container map should not cause issues
14521456 {
14531457 std::set<std::string> fullList;
14541458 std::unordered_map<std::string, std::shared_ptr<RawContainerInfo>> matchList;
14551459 ContainerFilters filters;
14561460 ContainerDiff diff;
1457-
1461+
14581462 // Should not crash with empty map
14591463 containerManager.computeMatchedContainersDiff (fullList, matchList, filters, false , diff);
14601464 EXPECT_EQ (diff.mAdded .size (), 0 );
14611465 EXPECT_EQ (diff.mRemoved .size (), 0 );
14621466 EXPECT_EQ (diff.mModified .size (), 0 );
14631467 }
1464-
1468+
14651469 // Test 2: Container with minimal info
14661470 {
14671471 RawContainerInfo minimalInfo;
14681472 minimalInfo.mID = " minimal1" ;
14691473 minimalInfo.mStatus = " running" ;
14701474 containerManager.mContainerMap [" minimal1" ] = std::make_shared<RawContainerInfo>(minimalInfo);
1471-
1475+
14721476 std::set<std::string> fullList;
14731477 std::unordered_map<std::string, std::shared_ptr<RawContainerInfo>> matchList;
14741478 ContainerFilters filters;
14751479 ContainerDiff diff;
1476-
1480+
14771481 containerManager.computeMatchedContainersDiff (fullList, matchList, filters, false , diff);
14781482 EXPECT_EQ (diff.mAdded .size (), 1 );
14791483 if (diff.mAdded .size () > 0 ) {
14801484 EXPECT_EQ (diff.mAdded [0 ]->mID , " minimal1" );
14811485 }
14821486 }
1483-
1487+
14841488 // Test 3: Multiple containers with various states
14851489 {
14861490 containerManager.mContainerMap .clear ();
1487-
1491+
14881492 RawContainerInfo running1;
14891493 running1.mID = " running1" ;
14901494 running1.mStatus = " running" ;
14911495 running1.mLogPath = " /var/log/running1" ;
14921496 containerManager.mContainerMap [" running1" ] = std::make_shared<RawContainerInfo>(running1);
1493-
1497+
14941498 RawContainerInfo exited1;
14951499 exited1.mID = " exited1" ;
14961500 exited1.mStatus = " exited" ;
14971501 exited1.mLogPath = " /var/log/exited1" ;
14981502 containerManager.mContainerMap [" exited1" ] = std::make_shared<RawContainerInfo>(exited1);
1499-
1503+
15001504 RawContainerInfo created1;
15011505 created1.mID = " created1" ;
15021506 created1.mStatus = " created" ;
15031507 created1.mLogPath = " /var/log/created1" ;
15041508 containerManager.mContainerMap [" created1" ] = std::make_shared<RawContainerInfo>(created1);
1505-
1509+
15061510 std::set<std::string> fullList;
15071511 std::unordered_map<std::string, std::shared_ptr<RawContainerInfo>> matchList;
15081512 ContainerFilters filters;
15091513 ContainerDiff diff;
1510-
1514+
15111515 // With isStdio=false, only running containers should be added
15121516 containerManager.computeMatchedContainersDiff (fullList, matchList, filters, false , diff);
15131517 EXPECT_EQ (diff.mAdded .size (), 1 );
15141518 EXPECT_EQ (fullList.size (), 1 ); // Only running container in fullList
15151519 if (diff.mAdded .size () > 0 ) {
15161520 EXPECT_EQ (diff.mAdded [0 ]->mID , " running1" );
15171521 }
1518-
1522+
15191523 // With isStdio=true, all containers should be added
15201524 fullList.clear ();
15211525 matchList.clear ();
@@ -1524,11 +1528,11 @@ void ContainerManagerUnittest::TestNullRawContainerInfoHandling() const {
15241528 EXPECT_EQ (diff2.mAdded .size (), 3 );
15251529 EXPECT_EQ (fullList.size (), 3 ); // All containers in fullList when isStdio=true
15261530 }
1527-
1531+
15281532 // Test 4: Verify thread-safe snapshot usage
15291533 {
15301534 containerManager.mContainerMap .clear ();
1531-
1535+
15321536 // Add some containers
15331537 for (int i = 0 ; i < 10 ; i++) {
15341538 RawContainerInfo info;
@@ -1537,19 +1541,19 @@ void ContainerManagerUnittest::TestNullRawContainerInfoHandling() const {
15371541 info.mLogPath = " /var/log/container" + std::to_string (i);
15381542 containerManager.mContainerMap [info.mID ] = std::make_shared<RawContainerInfo>(info);
15391543 }
1540-
1544+
15411545 std::set<std::string> fullList;
15421546 std::unordered_map<std::string, std::shared_ptr<RawContainerInfo>> matchList;
15431547 ContainerFilters filters;
15441548 ContainerDiff diff;
1545-
1549+
15461550 // This should use a snapshot and not be affected by concurrent modifications
15471551 containerManager.computeMatchedContainersDiff (fullList, matchList, filters, false , diff);
1548-
1552+
15491553 // Only running containers (even indices: 0, 2, 4, 6, 8) should be added
15501554 EXPECT_EQ (diff.mAdded .size (), 5 );
15511555 EXPECT_EQ (fullList.size (), 5 ); // Only running containers are added to fullList when isStdio=false
1552-
1556+
15531557 // Verify that only running containers are in fullList
15541558 for (int i = 0 ; i < 10 ; i++) {
15551559 std::string containerId = " container" + std::to_string (i);
@@ -1562,6 +1566,118 @@ void ContainerManagerUnittest::TestNullRawContainerInfoHandling() const {
15621566 }
15631567}
15641568
1569+ void ContainerManagerUnittest::TestConcurrentContainerMapAccess () const {
1570+ // This test simulates concurrent access to mContainerMap to reproduce race conditions
1571+ // that the fix was designed to prevent.
1572+ ContainerManager containerManager;
1573+
1574+ // Initialize with some containers
1575+ for (int i = 0 ; i < 20 ; i++) {
1576+ RawContainerInfo info;
1577+ info.mID = " container_" + std::to_string (i);
1578+ info.mStatus = " running" ;
1579+ info.mLogPath = " /var/log/container_" + std::to_string (i);
1580+ containerManager.mContainerMap [info.mID ] = std::make_shared<RawContainerInfo>(info);
1581+ }
1582+
1583+ std::atomic<bool > testRunning{true };
1584+ std::atomic<int > iterationCount{0 };
1585+ std::atomic<int > errorCount{0 };
1586+
1587+ // Thread 1: Continuously modify mContainerMap
1588+ auto modifyThread = [&]() {
1589+ int counter = 100 ;
1590+ while (testRunning) {
1591+ try {
1592+ // Add new containers
1593+ RawContainerInfo newInfo;
1594+ newInfo.mID = " dynamic_" + std::to_string (counter);
1595+ newInfo.mStatus = " running" ;
1596+ newInfo.mLogPath = " /var/log/dynamic_" + std::to_string (counter);
1597+
1598+ {
1599+ std::lock_guard<std::mutex> lock (containerManager.mContainerMapMutex );
1600+ containerManager.mContainerMap [newInfo.mID ] = std::make_shared<RawContainerInfo>(newInfo);
1601+ }
1602+
1603+ // Remove old containers
1604+ if (counter > 105 ) {
1605+ std::string oldId = " dynamic_" + std::to_string (counter - 5 );
1606+ std::lock_guard<std::mutex> lock (containerManager.mContainerMapMutex );
1607+ containerManager.mContainerMap .erase (oldId);
1608+ }
1609+
1610+ counter++;
1611+ std::this_thread::sleep_for (std::chrono::microseconds (100 ));
1612+ } catch (const std::exception& e) {
1613+ errorCount++;
1614+ }
1615+ }
1616+ };
1617+
1618+ // Thread 2: Continuously call computeMatchedContainersDiff (reads mContainerMap)
1619+ auto readThread = [&]() {
1620+ while (testRunning) {
1621+ try {
1622+ std::set<std::string> fullList;
1623+ std::unordered_map<std::string, std::shared_ptr<RawContainerInfo>> matchList;
1624+ ContainerFilters filters;
1625+ ContainerDiff diff;
1626+
1627+ // This should use a snapshot to avoid iterator invalidation
1628+ containerManager.computeMatchedContainersDiff (fullList, matchList, filters, false , diff);
1629+
1630+ iterationCount++;
1631+ std::this_thread::sleep_for (std::chrono::microseconds (100 ));
1632+ } catch (const std::exception& e) {
1633+ errorCount++;
1634+ }
1635+ }
1636+ };
1637+
1638+ // Thread 3: Another reader thread to increase contention
1639+ auto readThread2 = [&]() {
1640+ while (testRunning) {
1641+ try {
1642+ std::set<std::string> fullList;
1643+ std::unordered_map<std::string, std::shared_ptr<RawContainerInfo>> matchList;
1644+ ContainerFilters filters;
1645+ ContainerDiff diff;
1646+
1647+ containerManager.computeMatchedContainersDiff (fullList, matchList, filters, true , diff);
1648+
1649+ std::this_thread::sleep_for (std::chrono::microseconds (100 ));
1650+ } catch (const std::exception& e) {
1651+ errorCount++;
1652+ }
1653+ }
1654+ };
1655+
1656+ // Start threads
1657+ std::thread t1 (modifyThread);
1658+ std::thread t2 (readThread);
1659+ std::thread t3 (readThread2);
1660+
1661+ // Let them run for a short time
1662+ std::this_thread::sleep_for (std::chrono::milliseconds (500 ));
1663+
1664+ // Stop threads
1665+ testRunning = false ;
1666+
1667+ t1.join ();
1668+ t2.join ();
1669+ t3.join ();
1670+
1671+ // With the fix (snapshot), there should be no errors
1672+ EXPECT_EQ (errorCount.load (), 0 ) << " Concurrent access caused " << errorCount.load () << " errors" ;
1673+
1674+ // Verify we actually ran many iterations
1675+ EXPECT_GT (iterationCount.load (), 100 ) << " Test should run many iterations to stress test concurrency" ;
1676+
1677+ std::cout << " Concurrent test completed: " << iterationCount.load () << " iterations with " << errorCount.load ()
1678+ << " errors" << std::endl;
1679+ }
1680+
15651681void ContainerManagerUnittest::parseLabelFilters (const Json::Value& filtersJson, MatchCriteriaFilter& filter) const {
15661682 // Clear existing filters to ensure clean state
15671683 filter.mIncludeFields .mFieldsMap .clear ();
@@ -1625,6 +1741,7 @@ UNIT_TEST_CASE(ContainerManagerUnittest, TestSaveContainerInfoWithVersion)
16251741UNIT_TEST_CASE (ContainerManagerUnittest, TestContainerMatchingConsistency)
16261742UNIT_TEST_CASE (ContainerManagerUnittest, TestcomputeMatchedContainersDiffWithSnapshot)
16271743UNIT_TEST_CASE (ContainerManagerUnittest, TestNullRawContainerInfoHandling)
1744+ UNIT_TEST_CASE (ContainerManagerUnittest, TestConcurrentContainerMapAccess)
16281745
16291746} // namespace logtail
16301747
0 commit comments