| 
9 | 9 | 	"os"  | 
10 | 10 | 	"path/filepath"  | 
11 | 11 | 	"strings"  | 
 | 12 | +	"sync"  | 
12 | 13 | 	"testing"  | 
13 | 14 | 	"time"  | 
14 | 15 | 
 
  | 
@@ -105,7 +106,8 @@ func TestStopAtEOF(t *testing.T) {  | 
105 | 106 | 
 
  | 
106 | 107 | 	// Read to EOF  | 
107 | 108 | 	for i := 0; i < linesWrittenToFile-3; i++ {  | 
108 |  | -		<-tail.Lines  | 
 | 109 | +		line := <-tail.Lines  | 
 | 110 | +		tail.ReleaseLine(line)  | 
109 | 111 | 	}  | 
110 | 112 | 
 
  | 
111 | 113 | 	// Verify tail.Wait() has completed.  | 
@@ -163,11 +165,13 @@ func readThreelines(t *testing.T, tail *Tail) {  | 
163 | 165 | 		line := <-tail.Lines  | 
164 | 166 | 		if line.Err != nil {  | 
165 | 167 | 			t.Errorf("error tailing test file: %v", line.Err)  | 
 | 168 | +			tail.ReleaseLine(line) // Release even on error  | 
166 | 169 | 			continue  | 
167 | 170 | 		}  | 
168 | 171 | 		if !strings.HasSuffix(line.Text, "some log line") {  | 
169 | 172 | 			t.Errorf("wrong line from tail found: '%v'", line.Text)  | 
170 | 173 | 		}  | 
 | 174 | +		tail.ReleaseLine(line) // Release line back to pool  | 
171 | 175 | 	}  | 
172 | 176 | 	// If file was readable, then expect it to exist.  | 
173 | 177 | 	assert.Equal(t, int64(1), OpenFileCount.Load())  | 
@@ -238,6 +242,7 @@ func TestUtf16LineSize(t *testing.T) {  | 
238 | 242 | 	case line := <-tail.Lines:  | 
239 | 243 | 		// The line should be truncated to maxLineSize  | 
240 | 244 | 		assert.LessOrEqual(t, len(line.Text), maxLineSize)  | 
 | 245 | +		tail.ReleaseLine(line)  | 
241 | 246 | 	case <-time.After(1 * time.Second):  | 
242 | 247 | 		t.Fatal("timeout waiting for line")  | 
243 | 248 | 	}  | 
@@ -265,6 +270,7 @@ func TestTail_DefaultBuffer(t *testing.T) {  | 
265 | 270 | 	case line := <-tail.Lines:  | 
266 | 271 | 		assert.NoError(t, line.Err)  | 
267 | 272 | 		assert.Equal(t, normalContent, line.Text)  | 
 | 273 | +		tail.ReleaseLine(line)  | 
268 | 274 | 	case <-time.After(time.Second):  | 
269 | 275 | 		t.Fatal("Timeout waiting for line")  | 
270 | 276 | 	}  | 
@@ -292,7 +298,106 @@ func TestTail_1MBWithExplicitMaxLineSize(t *testing.T) {  | 
292 | 298 | 	case line := <-tail.Lines:  | 
293 | 299 | 		assert.NoError(t, line.Err)  | 
294 | 300 | 		assert.Equal(t, largeContent, line.Text)  | 
 | 301 | +		tail.ReleaseLine(line)  | 
295 | 302 | 	case <-time.After(time.Second):  | 
296 | 303 | 		t.Fatal("Timeout waiting for line")  | 
297 | 304 | 	}  | 
298 | 305 | }  | 
 | 306 | + | 
 | 307 | +// TestLinePooling verifies that Line objects are properly pooled and reused  | 
 | 308 | +func TestLinePooling(t *testing.T) {  | 
 | 309 | +	tmpfile, err := os.CreateTemp("", "pool_test")  | 
 | 310 | +	require.NoError(t, err)  | 
 | 311 | +	defer os.Remove(tmpfile.Name())  | 
 | 312 | + | 
 | 313 | +	content := "line1\nline2\nline3\n"  | 
 | 314 | +	err = os.WriteFile(tmpfile.Name(), []byte(content), 0600)  | 
 | 315 | +	require.NoError(t, err)  | 
 | 316 | + | 
 | 317 | +	tail, err := TailFile(tmpfile.Name(), Config{  | 
 | 318 | +		Follow:    false,  | 
 | 319 | +		MustExist: true,  | 
 | 320 | +	})  | 
 | 321 | +	require.NoError(t, err)  | 
 | 322 | +	defer tail.Stop()  | 
 | 323 | + | 
 | 324 | +	var lines []*Line  | 
 | 325 | +	for i := 0; i < 3; i++ {  | 
 | 326 | +		select {  | 
 | 327 | +		case line := <-tail.Lines:  | 
 | 328 | +			lines = append(lines, line)  | 
 | 329 | +		case <-time.After(time.Second):  | 
 | 330 | +			t.Fatal("Timeout waiting for line")  | 
 | 331 | +		}  | 
 | 332 | +	}  | 
 | 333 | + | 
 | 334 | +	assert.Equal(t, "line1", lines[0].Text)  | 
 | 335 | +	assert.Equal(t, "line2", lines[1].Text)  | 
 | 336 | +	assert.Equal(t, "line3", lines[2].Text)  | 
 | 337 | + | 
 | 338 | +	// Release all lines back to pool  | 
 | 339 | +	for _, line := range lines {  | 
 | 340 | +		tail.ReleaseLine(line)  | 
 | 341 | +	}  | 
 | 342 | + | 
 | 343 | +	// Line object should be zeroed out because we released it  | 
 | 344 | +	pooledLine := tail.linePool.Get().(*Line)  | 
 | 345 | +	assert.Empty(t, pooledLine.Text, "Pooled line should remain zeroed")  | 
 | 346 | +	assert.Empty(t, pooledLine.Time, "Pooled line should remain zeroed")  | 
 | 347 | +	assert.Empty(t, pooledLine.Err, "Pooled line should remain zeroed")  | 
 | 348 | +	assert.Empty(t, pooledLine.Offset, "Pooled line should remain zeroed")  | 
 | 349 | +	tail.ReleaseLine(pooledLine)  | 
 | 350 | +}  | 
 | 351 | + | 
 | 352 | +// TestConcurrentLinePoolAccess tests that the line pool is thread-safe  | 
 | 353 | +func TestConcurrentLinePoolAccess(t *testing.T) {  | 
 | 354 | +	tmpfile, err := os.CreateTemp("", "concurrent_test")  | 
 | 355 | +	require.NoError(t, err)  | 
 | 356 | +	defer os.Remove(tmpfile.Name())  | 
 | 357 | + | 
 | 358 | +	// Create content with multiple lines  | 
 | 359 | +	numLines := 100  | 
 | 360 | +	content := strings.Repeat("concurrent test line\n", numLines)  | 
 | 361 | +	err = os.WriteFile(tmpfile.Name(), []byte(content), 0600)  | 
 | 362 | +	require.NoError(t, err)  | 
 | 363 | + | 
 | 364 | +	tail, err := TailFile(tmpfile.Name(), Config{  | 
 | 365 | +		Follow:    false,  | 
 | 366 | +		MustExist: true,  | 
 | 367 | +	})  | 
 | 368 | +	require.NoError(t, err)  | 
 | 369 | +	defer tail.Stop()  | 
 | 370 | + | 
 | 371 | +	// Process lines concurrently  | 
 | 372 | +	var wg sync.WaitGroup  | 
 | 373 | +	linesChan := make(chan *Line, numLines)  | 
 | 374 | + | 
 | 375 | +	wg.Add(1)  | 
 | 376 | +	go func() {  | 
 | 377 | +		defer wg.Done()  | 
 | 378 | +		for i := 0; i < numLines; i++ {  | 
 | 379 | +			select {  | 
 | 380 | +			case line := <-tail.Lines:  | 
 | 381 | +				linesChan <- line  | 
 | 382 | +			case <-time.After(5 * time.Second):  | 
 | 383 | +				t.Errorf("Timeout waiting for line %d", i)  | 
 | 384 | +				return  | 
 | 385 | +			}  | 
 | 386 | +		}  | 
 | 387 | +		close(linesChan)  | 
 | 388 | +	}()  | 
 | 389 | + | 
 | 390 | +	numWorkers := 5  | 
 | 391 | +	for w := 0; w < numWorkers; w++ {  | 
 | 392 | +		wg.Add(1)  | 
 | 393 | +		go func() {  | 
 | 394 | +			defer wg.Done()  | 
 | 395 | +			for line := range linesChan {  | 
 | 396 | +				assert.Equal(t, "concurrent test line", line.Text)  | 
 | 397 | +				tail.ReleaseLine(line) // Release back to pool  | 
 | 398 | +			}  | 
 | 399 | +		}()  | 
 | 400 | +	}  | 
 | 401 | + | 
 | 402 | +	wg.Wait()  | 
 | 403 | +}  | 
0 commit comments