Skip to content

fix(search): log bleve's background errors - #3782

Open
junkerderprovinz wants to merge 2 commits into
opencloud-eu:mainfrom
junkerderprovinz:fix/issue-3594
Open

junkerderprovinz wants to merge 2 commits into
opencloud-eu:mainfrom
junkerderprovinz:fix/issue-3594

Conversation

@junkerderprovinz

@junkerderprovinz junkerderprovinz commented Oct 21, 2026 •

Copy link
Copy Markdown

Description

The search service opened its bleve index without an async error callback, so errors from scorch's merger, persister and introducer were dropped. The callback is registered once in the package's init(), because scorch reads its callback registry without a lock whenever an index opens; NewIndex only hands it the logger.

Errors are logged at error level with source, err_type and the index path. Logging is sampled to one line per minute for each source and error type, since a failing merge retries several times a second and that loop shouldn't hide a persister error. Panics are never sampled. Every error also increments opencloud_search_bleve_async_errors_total{source, err_type}.

The callback name is passed on every open, including creation (bleve.New became bleve.NewUsing), so a new index also stores it in index_meta.json.

Related Issue

This makes the problem visible but doesn't stop it: with a corrupt segment the merger still loops until bleve backs off (blevesearch/bleve#2402). Whether a failing index should mark the service unhealthy is left for the separate issue @dschmidt offered to open.

Motivation and Context

Thanks @sykoramaros for the thorough analysis and for pinning down the missing callback. This covers the logging and the metric you proposed.

How Has This Been Tested?

  • test environment: Windows, Go 1.26
  • go test ./services/search/... and go test -race on the bleve, mapping, search and query packages
  • specs: a new index carries the callback name in its index_meta.json; NewIndex leaves the registry alone; two indexes open at the same time without a race; a repeating error logs once while a different source or type logs right away; panics always log; the counter goes up per source and error type

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Technical debt
  • Tests only (no source changes)

Checklist:

  • Code changes
  • Unit tests added
  • Acceptance tests added
  • Documentation added

Claude Code helped with the investigation, the reproduction and the fix. I reviewed the change and take responsibility for it.

Assisted-by: Claude Code:claude-opus-5-5

The bleve index was opened without an async error callback, so errors
from scorch's merger and persister were dropped. A corrupt segment then
kept the merger rewriting the index with nothing in the logs. Log them,
at most once a minute, since a failing merge retries several times a
second. The retry loop itself stays until bleve backs off
(blevesearch/bleve#2402).

Fixes opencloud-eu#3594

Co-authored-by: Marosh <175698736+sykoramaros@users.noreply.github.com>
@codacy-production

codacy-production Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 critical

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
Security 1 critical

View in Codacy

🟢 Metrics 27 complexity

Metric Results
Complexity 27

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@dschmidt dschmidt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is the right fix for the silent half of #3594 and the maps.Clone reasoning checks out against bleve's newIndexUsing. Three things before merge:

1. Register the callback in init(), not on every NewIndex

scorch documents RegistryAsyncErrorCallbacks as read-only after process init, and NewScorch reads it without a lock. runtimeConfig writes it on every call, so a second concurrent NewIndex would race, and the function name hides the side effect. Suggestion: register once in the package's init() with a closure that reads the logger from a package-level atomic.Pointer[log.Logger] (or a small mutex-guarded struct), and let NewIndex only set that pointer. runtimeConfig then becomes a plain map again, and the repeated calls in the test suite stop rewriting the registry.

2. Sample per source and error type, never sample panics

One BurstSampler{Burst: 1, Period: 1m} is shared by everything, so a persister error (disk full, EIO) or a goroutine panic arriving within a minute of the looping merger error is dropped without a trace. The ScorchError you get already carries Source (merger, persister, introducer) and ErrType (persist, cleanup, async panic). Key the sampler on those two (bounded, at most nine keys, lazily created behind a mutex), log ErrAsyncPanic unsampled, and emit source and err_type as log fields. Each distinct failure then shows up immediately and only the identical retry loop is throttled.

3. Fixes vs Refs

The issue also asks for a metric or health signal. The metric half is cheap once you are inside the callback: a counter opencloud_search_bleve_async_errors_total{source, err_type} in services/search/pkg/metrics (same promauto pattern as the existing ones), incremented unsampled next to the sampled log line. If you add that, "Fixes" is fine for the log and metric part and I'll open a separate issue for the "mark unhealthy" question, which needs a policy decision (unready would take the whole search service out although queries still work). If you'd rather keep this PR to logging only, please change it to "Refs #3594".

scorch reads its callback registry without a lock when an index opens,
so the callback is registered once in init() and NewIndex only sets the
logger. A single sampler for all errors let the looping merger hide a
persister error or a panic, so sampling is now per source and error
type, and panics are never sampled. Every error also increments
opencloud_search_bleve_async_errors_total{source, err_type}.
@junkerderprovinz

Copy link
Copy Markdown
Author

Thanks for the careful review, all three points are in f377b4d. The callback is registered once in init(), and NewIndex only stores the logger in an atomic.Pointer, so nothing writes the registry after init. Sampling is keyed on source and error type, panics are never sampled, and every error increments opencloud_search_bleve_async_errors_total{source, err_type}. With the counter in place I kept "Fixes #3594"; the unhealthy question can go to your separate issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

search: bleve async errors are silently dropped, a corrupt segment turns the merger into an endless write loop

2 participants