@@ -28,6 +28,8 @@ extern "C" {
2828#include < set>
2929#include < random>
3030#include < unordered_set>
31+ #include < thread>
32+
3133/* *
3234 * The following tests purpose is to make sure the garbage collection is working properly,
3335 * without causing any data corruption or loss.
@@ -655,6 +657,81 @@ TEST_F(FGCTestTag, testDeleteDuringGCCleanup) {
655657 ASSERT_EQ (RSGlobalStats.totalStats .logically_deleted , 0 );
656658}
657659
660+ /* *
661+ * Test that simulates a pipe error during GC to trigger the error path.
662+ * This test verifies that the error handling doesn't cause double-free or other issues.
663+ */
664+ TEST_F (FGCTestTag, testPipeErrorDuringGC) {
665+ // Add some documents to create work for the GC
666+ ASSERT_TRUE (RS::addDocument (ctx, ism, " doc1" , " f1" , " hello" ));
667+ ASSERT_TRUE (RS::addDocument (ctx, ism, " doc2" , " f1" , " hello" ));
668+ ASSERT_TRUE (RS::addDocument (ctx, ism, " doc3" , " f1" , " hello" ));
669+
670+ FGC_WaitBeforeFork (fgc);
671+
672+ // Delete documents to trigger GC work
673+ ASSERT_TRUE (RS::deleteDocument (ctx, ism, " doc1" ));
674+ ASSERT_TRUE (RS::deleteDocument (ctx, ism, " doc2" ));
675+
676+ FGC_ForkAndWaitBeforeApply (fgc);
677+
678+ // Close the read end of the pipe from the parent's perspective
679+ // This will cause poll() to immediately return an error (POLLNVAL),
680+ // simulating a pipe failure scenario without waiting 3 minutes
681+ close (fgc->pipe_read_fd );
682+
683+ // This should handle the error gracefully without crashes or double-frees
684+ FGC_Apply (fgc);
685+
686+ // The GC should have failed, so no bytes should be collected
687+ // (or at least the operation should complete without crashing)
688+ ASSERT_EQ (0 , fgc->stats .totalCollected );
689+ }
690+
691+ /* *
692+ * Test that closes the pipe while GC is actively applying changes.
693+ * This test runs multiple iterations to increase the chance of hitting different
694+ * code paths and timing windows during the apply phase.
695+ */
696+ TEST_F (FGCTestTag, testPipeErrorDuringApply) {
697+ // Run multiple iterations to increase coverage of different timing scenarios
698+ for (int iteration = 0 ; iteration < 1000 ; iteration++) {
699+ // Add documents to create work for the GC
700+ std::string doc1 = " doc1_" + std::to_string (iteration);
701+ std::string doc2 = " doc2_" + std::to_string (iteration);
702+ std::string doc3 = " doc3_" + std::to_string (iteration);
703+
704+ ASSERT_TRUE (RS::addDocument (ctx, ism, doc1.c_str (), " f1" , " hello" ));
705+ ASSERT_TRUE (RS::addDocument (ctx, ism, doc2.c_str (), " f1" , " hello" ));
706+ ASSERT_TRUE (RS::addDocument (ctx, ism, doc3.c_str (), " f1" , " hello" ));
707+
708+ FGC_WaitBeforeFork (fgc);
709+
710+ // Delete documents to trigger GC work
711+ ASSERT_TRUE (RS::deleteDocument (ctx, ism, doc1.c_str ()));
712+ ASSERT_TRUE (RS::deleteDocument (ctx, ism, doc2.c_str ()));
713+
714+ FGC_ForkAndWaitBeforeApply (fgc);
715+
716+ // Start a thread to close the pipe after a brief delay
717+ // This creates a race condition where the pipe may be closed at various
718+ // points during the apply process
719+ std::thread closer ([this , iteration]() {
720+ // Variable delay to hit different code paths
721+ usleep (iteration);
722+ close (fgc->pipe_read_fd );
723+ });
724+
725+ // Apply should handle the pipe closure gracefully without crashing
726+ FGC_Apply (fgc);
727+
728+ closer.join ();
729+
730+ // Don't make any assertions about the state - it's timing dependent
731+ // The important thing is that we don't crash or have memory corruption
732+ }
733+ }
734+
658735TEST_F (FGCTestNumeric, testNumericBlocksSinceFork) {
659736 const auto startValue = TotalIIBlocks;
660737 constexpr size_t docs_per_block = INDEX_BLOCK_SIZE;
0 commit comments