|
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