Google Gemini real-time stream drops events after filter parse error

Problem – Real‑time Stream Drops Events After Filter Parse Error

In a Google Gemini deployment that ingests live video/audio streams, user‑defined filter expressions are evaluated on each incoming event to route content to moderation pipelines. Operators observed that after a certain point the pipeline silently stops processing events. The only clue in the Gemini service logs is a recurring filter_parser: parse error message. As a result, downstream moderation, analytics, and storage components receive gaps in the data stream.

Symptoms and Impact

  • Intermittent “event dropped” warnings in the streaming service metrics.
  • Log entries such as:
    
    2026-09-22T14:03:57.842Z ERROR filter_parser: failed to parse filter "type:video AND (duration>30" – unexpected token ')' at position 27
    2026-09-22T14:03:57.845Z WARN stream_processor: aborting processing of event 0x7f9c2a3b4c10 due to filter parse failure
    
  • Downstream systems (moderation, storage) report missing sequence numbers.
  • Overall latency spikes because the stream thread blocks while the parser retries.

Root Cause – Filter Expression Parser Throws Unrecoverable Exceptions

The Gemini real‑time pipeline uses a custom recursive‑descent parser written in Go. The parser expects filter strings to conform to a strict BNF grammar:

Token Description
IDENTIFIER Alphanumeric key (e.g., type, duration)
OPERATOR :, >, <=, AND, OR
LITERAL String or numeric literal
PARENTHESIS Grouping with ( and )

When a malformed filter reaches the parser, the implementation calls panic() instead of returning an error object. The panic propagates up to the stream‑processing goroutine, which catches it, logs the parse error, and aborts the current event without retrying. Because the parser is instantiated per‑event, a single malformed filter in a high‑throughput stream can cause thousands of consecutive events to be dropped until the offending filter string is replaced.

Typical user mistakes that trigger the panic:

  • Missing closing parenthesis.
  • Using unsupported logical operators (e.g., AND instead of && in older versions).
  • Embedding unescaped quotes inside string literals.
  • Trailing whitespace that the lexer does not trim.

Debug – Reproducing and Isolating the Failure

  1. Collect the offending filter strings. Query the Gemini configuration store for recent filter updates:
    
    gcloud firestore documents list \
      --collection=stream_filters \
      --order-by=updated_at \
      --limit=20
    
  2. Correlate logs with event IDs. Use Cloud Logging to extract events that were dropped:
    
    gcloud logging read '
      resource.type="cloud_run_revision"
      logName="projects/$PROJECT_ID/logs/gemini-stream"
      jsonPayload.message:"abort.*filter parse"
    ' --limit=100 --format=json
    
  3. Run the parser locally with the suspect string. The repository contains a test harness:
    
    #!/usr/bin/env bash
    FILTER="$1"
    go run ./internal/filter/parser/main.go "$FILTER"
    

    Example execution:

    
    $ ./run_parser.sh "type:video AND (duration>30"
    panic: unexpected token ')' at position 27
    
  4. Inspect the goroutine stack trace. The panic includes a stack dump:
    
    goroutine 42 [running]:
    github.com/google/gemini/internal/filter.Parse(0x0, 0x0, 0x0, 0x0)
        /src/internal/filter/parser.go:112 +0x3f
    github.com/google/gemini/internal/stream.processEvent(...)
        /src/internal/stream/processor.go:78
    
  5. Verify that the same filter works on a staging instance. Deploy the filter to a non‑production Gemini revision and observe that the parser succeeds when the syntax is corrected.

Solution – Make the Parser Robust and Enforce Validation Upstream

Code Changes

Replace the panic‑based error handling with a proper error return. The diff below shows the critical modifications:


// Before (filter/parser.go)
func Parse(input string) (*AST, error) {
    // ... lexer setup ...
    if token.Type == TOKEN_ERROR {
        panic(fmt.Sprintf("unexpected token %q at position %d", token.Literal, token.Pos))
    }
    // ...
}

// After (filter/parser.go)
func Parse(input string) (*AST, error) {
    // ... lexer setup ...
    if token.Type == TOKEN_ERROR {
        return nil, fmt.Errorf("unexpected token %q at position %d", token.Literal, token.Pos)
    }
    // ...
}

Update the stream processor to handle the error gracefully:


// Before (stream/processor.go)
ast, _ := filter.Parse(event.Filter)
// panic may have already aborted the goroutine

// After (stream/processor.go)
ast, err := filter.Parse(event.Filter)
if err != nil {
    logger.Errorf("filter_parser: %v – dropping event %s", err, event.ID)
    metrics.Inc("stream.events_dropped.filter_error")
    return // safely skip the event
}

Configuration Guardrails

Introduce a pre‑deployment validation webhook that rejects malformed filters before they reach the live stream:


apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: gemini-filter-validator
webhooks:
- name: filter.validate.gemini.google.com
  rules:
  - apiGroups: ["gemini.googleapis.com"]
    apiVersions: ["v1"]
    operations: ["CREATE","UPDATE"]
    resources: ["filters"]
  clientConfig:
    service:
      name: filter-validator
      namespace: gemini-system
      path: "/validate"
    caBundle: 

The webhook calls the same parser library and returns HTTP 400 if Parse yields an error.

Verify – Confirming the Fix Works in Production

  1. Deploy the updated Gemini container image (e.g., gcr.io/project-id/gemini-stream:20260923‑v2) to a canary revision.
  2. Submit a known‑bad filter via the API:
    
    curl -X POST https://gemini.example.com/v1/filters \
      -H "Authorization: Bearer $TOKEN" \
      -d '{"filter":"type:audio AND (duration>60"}'
    

    Expected response:

    
    {
      "error": {
        "code": 400,
        "message": "filter_parser: unexpected token ')' at position 27"
      }
    }
    
  3. Check that the canary logs contain no panic entries and that the metric stream.events_dropped.filter_error remains at zero for a 5‑minute window.
  4. Replay a subset of historic events with a deliberately malformed filter through the canary and verify that each event is logged as dropped but does not affect subsequent events.

Prevent – Operational Guardrails and Monitoring

  • Schema‑driven filter definition. Store filter expressions as a structured JSON object (field, operator, value) instead of free‑form strings. This eliminates parsing altogether.
  • Metric alerts. Create a Cloud Monitoring alert on stream.events_dropped.filter_error with a threshold of > 5 events per minute.
  • Automated regression tests. Add unit tests for every new operator or syntax change, exercising edge cases such as empty parentheses and escaped quotes.
  • Version pinning. Document the supported operator set per Gemini release and enforce it via the validation webhook.
  • Observability. Emit a structured log field filter_error_detail to enable quick correlation with the offending filter ID.

Related Topic Hub: LLM Systems Troubleshooting Hub

FAQ

  1. Why does the stream continue processing after a parse error instead of crashing the whole service?

    The parser panic was previously uncaught, causing the goroutine handling the current event to abort. Other goroutines remained alive, so the service stayed up but silently dropped events.

  2. Can I use logical operators like && and || in filters?

    Only the canonical keywords AND and OR are supported in the current Gemini version. Attempting to use && or || triggers a parse error.

  3. How do I migrate existing free‑form filters to the structured JSON format?

    Write a one‑time migration script that parses each stored filter with the updated parser, extracts the AST, and persists a JSON representation. Invalid filters should be flagged for manual review.

  4. Is there a way to see which filter caused a specific event drop?

    Enable the filter_id field in the event payload and include it in the error log:

    
    logger.Errorf("filter_parser: %v – dropping event %s (filter_id=%s)", err, event.ID, event.FilterID)
    

    This allows you to query Cloud Logging for the exact filter ID.

  5. Will the validation webhook add noticeable latency to filter creation?

    The webhook runs the same in‑process parser, which completes in sub‑millisecond time for typical filter lengths. Observed latency increase is < 5 ms, well within API SLA.