@@ -35,9 +35,37 @@ class CheckpointManagerUnittest : public ::testing::Test {
3535 static void TearDownTestCase () { bfs::remove_all (kTestRootDir ); }
3636
3737 void TestSearchFilePathByDevInodeInDirectory ();
38+ void TestAddCheckPointStaleFileNotOverwriteActive ();
39+ void TestAddCheckPointActiveOverwriteStale ();
40+ void TestAddCheckPointBothExistLastWriterWins ();
41+ void TestAddCheckPointNeitherExistLastWriterWins ();
42+
43+ private:
44+ static CheckPoint* MakeCheckPoint (const std::string& fileName,
45+ const DevInode& devInode,
46+ int64_t offset,
47+ const std::string& configName,
48+ const std::string& realFileName = " " ) {
49+ return new CheckPoint (fileName,
50+ " " /* resolvedFileName */ ,
51+ offset,
52+ 0 /* signatureSize */ ,
53+ 0 /* signatureHash */ ,
54+ devInode,
55+ configName,
56+ realFileName,
57+ false /* fileOpenFlag */ ,
58+ false /* containerStopped */ ,
59+ " " /* containerID */ ,
60+ false /* lastForceRead */ );
61+ }
3862};
3963
4064UNIT_TEST_CASE (CheckpointManagerUnittest, TestSearchFilePathByDevInodeInDirectory);
65+ UNIT_TEST_CASE (CheckpointManagerUnittest, TestAddCheckPointStaleFileNotOverwriteActive);
66+ UNIT_TEST_CASE (CheckpointManagerUnittest, TestAddCheckPointActiveOverwriteStale);
67+ UNIT_TEST_CASE (CheckpointManagerUnittest, TestAddCheckPointBothExistLastWriterWins);
68+ UNIT_TEST_CASE (CheckpointManagerUnittest, TestAddCheckPointNeitherExistLastWriterWins);
4169
4270void CheckpointManagerUnittest::TestSearchFilePathByDevInodeInDirectory () {
4371 const std::string kRotateFileName = " test.log.5" ;
@@ -96,6 +124,106 @@ void CheckpointManagerUnittest::TestSearchFilePathByDevInodeInDirectory() {
96124 }
97125}
98126
127+ // Fake rotation: a new file B reuses the inode of a deleted file A. Both readers dump a
128+ // checkpoint under the same (dev, inode, config) key. The stale checkpoint of the deleted
129+ // file A must not overwrite the active checkpoint of the still-existing file B.
130+ void CheckpointManagerUnittest::TestAddCheckPointStaleFileNotOverwriteActive () {
131+ CheckPointManager::Instance ()->RemoveAllCheckPoint ();
132+ const std::string configName = " test-config" ;
133+ const std::string activeFile = (bfs::path (kTestRootDir ) / " active.log" ).string ();
134+ const std::string deletedFile = (bfs::path (kTestRootDir ) / " deleted.log" ).string ();
135+ std::ofstream (activeFile) << " active" ;
136+ // deletedFile is never created on disk -> its checkpoint is stale.
137+
138+ fsutil::PathStat ps;
139+ EXPECT_TRUE (fsutil::PathStat::stat (activeFile, ps));
140+ const DevInode reusedDevInode = ps.GetDevInode ();
141+
142+ // Active reader dumps first, then the stale reader of the deleted file (same inode).
143+ CheckPointManager::Instance ()->AddCheckPoint (MakeCheckPoint (activeFile, reusedDevInode, 1000 , configName));
144+ CheckPointManager::Instance ()->AddCheckPoint (MakeCheckPoint (deletedFile, reusedDevInode, 0 , configName));
145+
146+ CheckPointPtr cpt;
147+ EXPECT_TRUE (CheckPointManager::Instance ()->GetCheckPoint (reusedDevInode, configName, cpt));
148+ EXPECT_EQ (cpt->mFileName , activeFile);
149+ EXPECT_EQ (cpt->mOffset , 1000 );
150+ EXPECT_EQ (CheckPointManager::Instance ()->GetAllFileCheckPoint ().size (), 1UL );
151+
152+ CheckPointManager::Instance ()->RemoveAllCheckPoint ();
153+ bfs::remove (activeFile);
154+ }
155+
156+ // Order independence: even when the stale checkpoint is added first, the later active
157+ // checkpoint of the still-existing file must win.
158+ void CheckpointManagerUnittest::TestAddCheckPointActiveOverwriteStale () {
159+ CheckPointManager::Instance ()->RemoveAllCheckPoint ();
160+ const std::string configName = " test-config" ;
161+ const std::string activeFile = (bfs::path (kTestRootDir ) / " active2.log" ).string ();
162+ const std::string deletedFile = (bfs::path (kTestRootDir ) / " deleted2.log" ).string ();
163+ std::ofstream (activeFile) << " active" ;
164+
165+ fsutil::PathStat ps;
166+ EXPECT_TRUE (fsutil::PathStat::stat (activeFile, ps));
167+ const DevInode reusedDevInode = ps.GetDevInode ();
168+
169+ CheckPointManager::Instance ()->AddCheckPoint (MakeCheckPoint (deletedFile, reusedDevInode, 0 , configName));
170+ CheckPointManager::Instance ()->AddCheckPoint (MakeCheckPoint (activeFile, reusedDevInode, 2000 , configName));
171+
172+ CheckPointPtr cpt;
173+ EXPECT_TRUE (CheckPointManager::Instance ()->GetCheckPoint (reusedDevInode, configName, cpt));
174+ EXPECT_EQ (cpt->mFileName , activeFile);
175+ EXPECT_EQ (cpt->mOffset , 2000 );
176+ EXPECT_EQ (CheckPointManager::Instance ()->GetAllFileCheckPoint ().size (), 1UL );
177+
178+ CheckPointManager::Instance ()->RemoveAllCheckPoint ();
179+ bfs::remove (activeFile);
180+ }
181+
182+ // Both files exist (e.g. hard links sharing an inode): legacy last-writer-wins is preserved.
183+ void CheckpointManagerUnittest::TestAddCheckPointBothExistLastWriterWins () {
184+ CheckPointManager::Instance ()->RemoveAllCheckPoint ();
185+ const std::string configName = " test-config" ;
186+ const std::string file1 = (bfs::path (kTestRootDir ) / " link1.log" ).string ();
187+ const std::string file2 = (bfs::path (kTestRootDir ) / " link2.log" ).string ();
188+ std::ofstream (file1) << " data" ;
189+ bfs::create_hard_link (file1, file2);
190+
191+ fsutil::PathStat ps;
192+ EXPECT_TRUE (fsutil::PathStat::stat (file1, ps));
193+ const DevInode devInode = ps.GetDevInode ();
194+
195+ CheckPointManager::Instance ()->AddCheckPoint (MakeCheckPoint (file1, devInode, 100 , configName));
196+ CheckPointManager::Instance ()->AddCheckPoint (MakeCheckPoint (file2, devInode, 200 , configName));
197+
198+ CheckPointPtr cpt;
199+ EXPECT_TRUE (CheckPointManager::Instance ()->GetCheckPoint (devInode, configName, cpt));
200+ EXPECT_EQ (cpt->mFileName , file2);
201+ EXPECT_EQ (cpt->mOffset , 200 );
202+
203+ CheckPointManager::Instance ()->RemoveAllCheckPoint ();
204+ bfs::remove (file1);
205+ bfs::remove (file2);
206+ }
207+
208+ // Neither file exists (both stale): fall back to legacy last-writer-wins, no state dropped.
209+ void CheckpointManagerUnittest::TestAddCheckPointNeitherExistLastWriterWins () {
210+ CheckPointManager::Instance ()->RemoveAllCheckPoint ();
211+ const std::string configName = " test-config" ;
212+ const std::string gone1 = (bfs::path (kTestRootDir ) / " gone1.log" ).string ();
213+ const std::string gone2 = (bfs::path (kTestRootDir ) / " gone2.log" ).string ();
214+ const DevInode devInode (12345 , 67890 ); // does not match any real file
215+
216+ CheckPointManager::Instance ()->AddCheckPoint (MakeCheckPoint (gone1, devInode, 100 , configName));
217+ CheckPointManager::Instance ()->AddCheckPoint (MakeCheckPoint (gone2, devInode, 200 , configName));
218+
219+ CheckPointPtr cpt;
220+ EXPECT_TRUE (CheckPointManager::Instance ()->GetCheckPoint (devInode, configName, cpt));
221+ EXPECT_EQ (cpt->mFileName , gone2);
222+ EXPECT_EQ (cpt->mOffset , 200 );
223+
224+ CheckPointManager::Instance ()->RemoveAllCheckPoint ();
225+ }
226+
99227} // namespace logtail
100228
101229UNIT_TEST_MAIN
0 commit comments