Skip to content

Boundary and Persister Service changes - #1377

Open
ritikegov wants to merge 27 commits into
masterfrom
master-perf
Open

Boundary and Persister Service changes #1377
ritikegov wants to merge 27 commits into
masterfrom
master-perf

Conversation

@ritikegov

@ritikegov ritikegov commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added POST /boundary-relationships/bulk/_create with per-record success/failure reporting and bulk limits.
    • Added bulk-create Kafka routing and bulk payload handling for efficient processing.
    • Improved Kafka log tracing by propagating correlation/tenant context across produce/consume.
  • Bug Fixes

    • Hardened bulk/single query and validation to correctly handle duplicate codes and reject invalid boundary/hierarchy inputs.
  • Reliability

    • Upgraded persister processing with transient vs permanent failure handling, DB health pause/resume, record-level poison isolation for bulk payloads, and durable dead-letter/parking.
    • Made multiple persistence inserts idempotent (ON CONFLICT DO NOTHING).
    • Added/optimized boundary relationship search indexes for subtree queries.

holashchand and others added 15 commits June 26, 2026 10:27
…ter + boundary-service)

05f6889 also carried ON CONFLICT edits to persister YAMLs in unrelated
modules (audit-service, egov-pg-service, egov-workflow-v2, mdms-v2,
service-request). These are out of scope for the persister RCA reliability fix
and the boundary-relationship enhancement, so they are reverted to their
prior state. This branch now touches only egov-persister and boundary-service.
BoundaryRelationshipMappingShapeTest was a local verification harness used to
prove the dedicated-bulk-topic fix (single-object vs array extraction). Its
purpose is served, and a boundary-relationship-specific test does not belong in
the generic egov-persister module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Boundary Service adds bulk boundary-relationship creation with synchronous validation and Kafka publication. egov-persister adds batch processing, failure classification, retry/dead-letter routing, database health monitoring, aggregated persistence, and conflict-safe inserts. Tracer adds Kafka MDC propagation.

Changes

Boundary bulk creation

Layer / File(s) Summary
Bulk API validation and Kafka publication
core-services/boundary-service/src/main/java/digit/{web,service,repository}/*, .../resources/application.properties, README.md
Adds the bulk API contract, per-record validation and failures, duplicate detection, keyed or keyless batch publication, configuration limits, hierarchy-name guards, and documentation.
Boundary persistence and search support
core-services/boundary-service/src/main/resources/boundary-persister.yml, .../repository/querybuilder/*, .../db/migration/*
Adds conflict-safe boundary inserts, deduplicated query parameters, and subtree-search indexes.

Persister reliability and batching

Layer / File(s) Summary
Persistence classification and failure routing
core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/*, .../test/java/.../consumer/*
Adds failure classification, retries, record isolation, dead-letter and parking flows, and tests for single and bulk failure handling.
Consumer lifecycle and batch persistence
core-services/egov-persister/src/main/java/org/egov/infra/persist/{consumer,repository,service}/*, .../resources/*
Adds explicit consumer lifecycle, parallel topic processing, database-health gating, lenient extraction, aggregated writes, and idempotent SQL mappings.

Kafka tracing

Layer / File(s) Summary
Kafka MDC propagation
core-services/libraries/tracer/src/main/java/org/egov/tracer/{config,kafka}/*, .../resources/tracer.properties, readme.md
Adds Kafka header stamping, per-record MDC reconstruction and cleanup, and a configurable interceptor.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant BoundaryService
  participant Kafka
  participant Persister
  participant Database
  Client->>BoundaryService: Submit bulk boundary relationships
  BoundaryService->>BoundaryService: Validate and enrich records
  BoundaryService->>Kafka: Publish batch relationship message
  Kafka->>Persister: Deliver batch message
  Persister->>Database: Aggregate and persist rows
  Database-->>Persister: Success or classified failure
  Persister-->>Kafka: Retry, dead-letter, or park failed records
Loading

Suggested reviewers: talele08, ghanshyamrawat-egov

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related but too generic; it names the services without summarizing the main bulk and reliability changes. Rename it to a specific summary like: "Add bulk boundary relationship create flow and persister reliability updates".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch master-perf

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (3)
core-services/boundary-service/src/main/java/digit/web/models/BulkBoundaryRelationshipRequest.java (1)

40-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the hardcoded @Size constraint to avoid conflicts with programmatic configuration.

The @Size(min = 1, max = 100) constraint hardcodes the maximum batch size to 100. If Bean Validation is ever enabled in the deployment, this will conflict with the configurable boundary.bulk.max.size property (which defaults to 100 but can be changed). Since the service explicitly enforces the configurable size limit programmatically, consider removing the @Size constraint to prevent potential conflicts and ensure the configuration property remains the single source of truth.

♻️ Proposed refactor

Remove the annotation:

-    `@Size`(min = 1, max = 100)

You can optionally remove the unused import at the top of the file as well:

import jakarta.validation.constraints.Size;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/boundary-service/src/main/java/digit/web/models/BulkBoundaryRelationshipRequest.java`
at line 40, Remove the hardcoded `@Size` constraint from
BulkBoundaryRelationshipRequest and delete its now-unused Size import. Keep
batch-size validation in the existing programmatic enforcement that uses the
configurable boundary.bulk.max.size property.
core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterMessageListenerTest.java (1)

167-189: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise permanent-before-transient ordering.

The current slow, bad order throws before any DLQ side effect. Add bad, slow and verify that a later transient failure does not leave an already-published DLQ entry before the original message is retried.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterMessageListenerTest.java`
around lines 167 - 189, Extend
transientFailureDuringIsolationRethrowsAndNothingIsDeadLettered to exercise the
bad, slow ordering, ensuring the first permanent record is processed before the
later transient failure. Assert that the listener rethrows
TransientPersistException and kafkaTemplate.send is never invoked, so no DLQ
entry is published before the original message is retried.
core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterBatchListnerRecordIsolationTest.java (1)

123-145: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add the reverse isolation order.

Test [bad, slow] as well as [slow, bad]; the former exposes whether a permanent record is dead-lettered before the later transient failure forces the whole poll to replay.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterBatchListnerRecordIsolationTest.java`
around lines 123 - 145, Extend
transientFailureDuringIsolationRethrowsForInPlaceRetry to also invoke
listener.onMessage with the records ordered as [bad, slow]. Assert that it
throws TransientPersistException and verify kafkaTemplate never sends to DLQ for
this reverse isolation order, preserving the existing [slow, bad] assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/DbExceptionClassifier.java`:
- Around line 30-49: Remove SQLState 57014 from the transient classification in
DbExceptionClassifier, while preserving the other transient SQLState checks. In
DbExceptionClassifierTest, remove the assertion that expects 57014 to be
transient; no direct change is needed for the remaining test cases.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchConsumerConfig.java`:
- Around line 112-115: The independently started batchContainer is not
controlled by DbHealthMonitor during database outages. Add idempotent pause and
resume operations for batchContainer in PersisterBatchConsumerConfig, then
update the monitor integration to invoke those operations alongside
PersisterConsumerConfig so both listener containers stop and restart polling
together.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchListner.java`:
- Around line 195-216: Update the duplicate-handling branches in the batch and
single-record paths around DbExceptionClassifier.Kind.BENIGN to track
already-existing records separately from newly persisted records instead of
adding them to persisted. Keep persisted limited to records that were actually
committed so the sendAudit call only emits events for new writes.
- Around line 172-211: Immediate DLQ publication in the record-isolation flow
can duplicate permanent failures when a later transient error retries the batch.
In
core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchListner.java:172-211,
stage isolated DLQ records until the entire sweep completes, or publish them
idempotently using source offsets. Add permanent-before-transient ordering
coverage in
core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterMessageListenerTest.java:167-189
and
core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterBatchListnerRecordIsolationTest.java:123-145.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterConsumerConfig.java`:
- Around line 163-166: Update the customExecutorEnabled branch in
PersisterConsumerConfig so the fixed-thread-pool ExecutorService is
lifecycle-managed: either expose it as a managed bean or register shutdown when
the KafkaMessageListenerContainer stops. Do not create an unmanaged local
executor, and preserve the existing listener task-executor configuration.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterMessageListener.java`:
- Around line 149-169: The record-isolation loop in PersisterMessageListener
must defer DLQ and parking publication until the entire sweep completes without
a transient failure. Stage each permanent failure and its retry metadata during
the loop, preserve benign/transient handling, then publish the staged entries
only after the loop succeeds, updating deadLettered and parked counts at
publication time.
- Around line 70-73: Update the attempts parsing in PersisterMessageListener so
only integral numeric values within the valid retry-counter range are accepted;
treat negative, overflowing, non-integral, and non-numeric values as maxRetries.
Avoid Number.intValue() silently narrowing invalid values, while preserving the
existing ceiling behavior for foreign or raw DLQ records.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterProducerConfig.java`:
- Around line 62-71: Update the parking-envelope construction in the producer
error-handling flow to populate parked.body with the original raw bytes from the
deserialization exception header when record.value() is null, while preserving
the existing value for successfully deserialized records. Use the available
exception/header metadata associated with the failed record so poison messages
remain replayable.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/service/PersistService.java`:
- Around line 118-129: Remove the try-catch around persistRepository.getRows in
the document-processing loop so extraction exceptions propagate to the upstream
listener for record-level isolation and DLQ routing. Keep the existing
skippedDocuments increment for empty results, and do not increment it or
log-and-continue when getRows fails.
- Around line 74-88: Replace the documentToMappings LinkedHashMap in the persist
flow with a list of document-to-mappings entries so identical parsed JSON
documents remain separate batch items. Update the subsequent iteration to
process each entry in insertion order while preserving every message’s
applicable mappings and exact multiplicity.

---

Nitpick comments:
In
`@core-services/boundary-service/src/main/java/digit/web/models/BulkBoundaryRelationshipRequest.java`:
- Line 40: Remove the hardcoded `@Size` constraint from
BulkBoundaryRelationshipRequest and delete its now-unused Size import. Keep
batch-size validation in the existing programmatic enforcement that uses the
configurable boundary.bulk.max.size property.

In
`@core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterBatchListnerRecordIsolationTest.java`:
- Around line 123-145: Extend
transientFailureDuringIsolationRethrowsForInPlaceRetry to also invoke
listener.onMessage with the records ordered as [bad, slow]. Assert that it
throws TransientPersistException and verify kafkaTemplate never sends to DLQ for
this reverse isolation order, preserving the existing [slow, bad] assertions.

In
`@core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterMessageListenerTest.java`:
- Around line 167-189: Extend
transientFailureDuringIsolationRethrowsAndNothingIsDeadLettered to exercise the
bad, slow ordering, ensuring the first permanent record is processed before the
later transient failure. Assert that the listener rethrows
TransientPersistException and kafkaTemplate.send is never invoked, so no DLQ
entry is published before the original message is retried.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa854349-5b40-43e0-b5a7-677c9df1b889

📥 Commits

Reviewing files that changed from the base of the PR and between e22c7c5 and 7c8d3ea.

📒 Files selected for processing (48)
  • core-services/boundary-service/README.md
  • core-services/boundary-service/src/main/java/digit/config/ApplicationProperties.java
  • core-services/boundary-service/src/main/java/digit/errors/ErrorCodes.java
  • core-services/boundary-service/src/main/java/digit/kafka/Producer.java
  • core-services/boundary-service/src/main/java/digit/repository/BoundaryRelationshipRepository.java
  • core-services/boundary-service/src/main/java/digit/repository/impl/BoundaryRelationshipRepositoryImpl.java
  • core-services/boundary-service/src/main/java/digit/repository/querybuilder/BoundaryEntityQueryBuilder.java
  • core-services/boundary-service/src/main/java/digit/repository/querybuilder/BoundaryRelationshipQueryBuilder.java
  • core-services/boundary-service/src/main/java/digit/service/BoundaryRelationshipService.java
  • core-services/boundary-service/src/main/java/digit/service/validator/BoundaryRelationshipValidator.java
  • core-services/boundary-service/src/main/java/digit/util/HierarchyUtil.java
  • core-services/boundary-service/src/main/java/digit/web/controllers/BoundaryRelationshipController.java
  • core-services/boundary-service/src/main/java/digit/web/models/BulkBoundaryRelationshipRequest.java
  • core-services/boundary-service/src/main/java/digit/web/models/BulkBoundaryRelationshipRequestDTO.java
  • core-services/boundary-service/src/main/java/digit/web/models/BulkBoundaryRelationshipResponse.java
  • core-services/boundary-service/src/main/java/digit/web/models/FailedBoundaryRelationship.java
  • core-services/boundary-service/src/main/resources/application.properties
  • core-services/boundary-service/src/main/resources/boundary-persister.yml
  • core-services/boundary-service/src/main/resources/db/migration/main/V20260616120000__boundary_relationship_search_indexes.sql
  • core-services/boundary-service/src/test/java/digit/repository/impl/BoundaryRelationshipRepositoryImplBulkTest.java
  • core-services/egov-persister/src/main/java/org/egov/EgovPersistApplication.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/DbExceptionClassifier.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/DbHealthMonitor.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchConsumerConfig.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchListner.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterConsumerConfig.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterMessageListener.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterProducerConfig.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/RecordSplitter.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/StoppingErrorHandler.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/TransientPersistException.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/repository/PersistRepository.java
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/service/PersistService.java
  • core-services/egov-persister/src/main/resources/application.properties
  • core-services/egov-persister/src/main/resources/egov-pg-service-persister.yml
  • core-services/egov-persister/src/main/resources/egov-user-event-persister.yml
  • core-services/egov-persister/src/main/resources/hrms-employee-persister.yml
  • core-services/egov-persister/src/main/resources/persister.yml
  • core-services/egov-persister/src/main/resources/pgr.v3.yml
  • core-services/egov-persister/src/main/resources/property-services.yml
  • core-services/egov-persister/src/main/resources/pt-drafts.yml
  • core-services/egov-persister/src/main/resources/tl-billing-slab-persister.yml
  • core-services/egov-persister/src/main/resources/user-service-persist.yml
  • core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/DbExceptionClassifierTest.java
  • core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterBatchConsumerConfigTest.java
  • core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterBatchListnerRecordIsolationTest.java
  • core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterMessageListenerTest.java
  • core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/RecordSplitterTest.java
💤 Files with no reviewable changes (2)
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/StoppingErrorHandler.java
  • core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterBatchConsumerConfigTest.java

Comment on lines +112 to +115
batchContainer = new KafkaMessageListenerContainer<>(createConsumerFactory(), properties);
batchContainer.setCommonErrorHandler(persisterErrorHandler);
batchContainer.setBeanName("batchContainer");
batchContainer.start();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Include the batch container in database-health pause/resume.

DbHealthMonitor only controls PersisterConsumerConfig, so this independently started batch container continues polling during a database outage. Expose idempotent pause/resume operations here and have the monitor control both containers, or introduce a shared container registry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchConsumerConfig.java`
around lines 112 - 115, The independently started batchContainer is not
controlled by DbHealthMonitor during database outages. Add idempotent pause and
resume operations for batchContainer in PersisterBatchConsumerConfig, then
update the monitor integration to invoke those operations alongside
PersisterConsumerConfig so both listener containers stop and restart polling
together.

Comment on lines +172 to 211
for (String message : messages) {
try {
persistService.persist(topic, Collections.singletonList(message));
persisted.add(message);
} catch (Exception ex) {
DbExceptionClassifier.Kind kind = DbExceptionClassifier.classify(ex);
if (kind == DbExceptionClassifier.Kind.TRANSIENT) {
// DB went down partway through isolation: abort and retry the whole batch rather
// than parking the remaining good records as if they were poison.
throw new TransientPersistException("Transient DB failure isolating record for topic " + topic, ex);
}
else {
topicTorcvDataList.get(data.topic()).add(objectMapper.writeValueAsString(data.value()));
// A bulk producer publishes a whole list as ONE message, so isolate WITHIN the
// message (R1 at record granularity): good sibling rows must not share a poison
// row's dead-letter, and a BENIGN duplicate must not absorb not-yet-persisted
// siblings (the array insert aborts on the duplicate before reaching them).
List<String> records = RecordSplitter.split(message);
if (records != null) {
for (String record : records) {
try {
persistService.persist(topic, Collections.singletonList(record));
persisted.add(record);
} catch (Exception rex) {
DbExceptionClassifier.Kind recordKind = DbExceptionClassifier.classify(rex);
if (recordKind == DbExceptionClassifier.Kind.BENIGN) {
persisted.add(record); // already present -> idempotent success
} else if (recordKind == DbExceptionClassifier.Kind.TRANSIENT) {
throw new TransientPersistException("Transient DB failure isolating record for topic " + topic, rex);
} else {
sendToDlq(topic, record, 0, rex); // only the offending record
failed++;
}
}
}
} else if (kind == DbExceptionClassifier.Kind.BENIGN) {
persisted.add(message); // single already-present record -> idempotent success
} else {
sendToDlq(topic, message, 0, ex); // unsplittable payload: message-level isolation
failed++;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Immediate DLQ side effects are unsafe before a potentially transient sweep completes. A later transient failure replays the original poll, duplicating any permanent records already published to the DLQ.

  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchListner.java#L172-L211: stage DLQ entries until isolation completes, or publish them idempotently using source offsets.
  • core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterMessageListenerTest.java#L167-L189: add permanent-before-transient ordering.
  • core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterBatchListnerRecordIsolationTest.java#L123-L145: add the same reverse-order regression case.
📍 Affects 3 files
  • core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchListner.java#L172-L211 (this comment)
  • core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterMessageListenerTest.java#L167-L189
  • core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterBatchListnerRecordIsolationTest.java#L123-L145
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchListner.java`
around lines 172 - 211, Immediate DLQ publication in the record-isolation flow
can duplicate permanent failures when a later transient error retries the batch.
In
core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchListner.java:172-211,
stage isolated DLQ records until the entire sweep completes, or publish them
idempotently using source offsets. Add permanent-before-transient ordering
coverage in
core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterMessageListenerTest.java:167-189
and
core-services/egov-persister/src/test/java/org/egov/infra/persist/consumer/PersisterBatchListnerRecordIsolationTest.java:123-145.

Comment on lines +195 to +216
if (recordKind == DbExceptionClassifier.Kind.BENIGN) {
persisted.add(record); // already present -> idempotent success
} else if (recordKind == DbExceptionClassifier.Kind.TRANSIENT) {
throw new TransientPersistException("Transient DB failure isolating record for topic " + topic, rex);
} else {
sendToDlq(topic, record, 0, rex); // only the offending record
failed++;
}
}
}
} else if (kind == DbExceptionClassifier.Kind.BENIGN) {
persisted.add(message); // single already-present record -> idempotent success
} else {
sendToDlq(topic, message, 0, ex); // unsplittable payload: message-level isolation
failed++;
}
}
catch (JsonProcessingException e) {
log.error("Failed to serialize incoming message", e);
}
});

for(Map.Entry<String,List<String>> entry : topicTorcvDataList.entrySet()){
persistService.persist(entry.getKey(),entry.getValue());
if(!entry.getKey().equalsIgnoreCase(persistAuditKafkaTopic)){
Map<String, Object> producerRecord = new HashMap<>();
producerRecord.put("topic", entry.getKey());
producerRecord.put("value", entry.getValue());
kafkaTemplate.send(auditGenerateKafkaTopic, producerRecord);
}
}

if (!persisted.isEmpty()) {
sendAudit(topic, persisted);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Exclude benign duplicates from the post-commit audit.

Lines 195 and 205 add already-existing rows to persisted, so Line 216 emits audit events even though no write occurred. Track handled duplicates separately and audit only newly committed rows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchListner.java`
around lines 195 - 216, Update the duplicate-handling branches in the batch and
single-record paths around DbExceptionClassifier.Kind.BENIGN to track
already-existing records separately from newly persisted records instead of
adding them to persisted. Keep persisted limited to records that were actually
committed so the sendAudit call only emits events for new writes.

Comment on lines +163 to +166
if (customExecutorEnabled) {
ExecutorService executorService = Executors.newFixedThreadPool(maxPoolSize);
AsyncTaskExecutor taskExecutor = new ConcurrentTaskExecutor(executorService);
properties.setListenerTaskExecutor(taskExecutor);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterConsumerConfig.java'

echo "== outline =="
ast-grep outline "$file" --view expanded || true

echo
echo "== relevant lines =="
sed -n '130,220p' "$file" | cat -n

echo
echo "== search for container type / concurrency / executor lifecycle =="
rg -n "KafkaMessageListenerContainer|ConcurrentMessageListenerContainer|setListenerTaskExecutor|setConcurrency|customExecutorEnabled|maxPoolSize|destroy|shutdown|ExecutorService" core-services/egov-persister/src/main/java

Repository: egovernments/Digit-Core

Length of output: 8570


🌐 Web query:

Spring Kafka ContainerProperties setListenerTaskExecutor polling thread lifecycle shutdown executor ownership

💡 Result:

In Spring Kafka, ContainerProperties.setListenerTaskExecutor is used to configure the AsyncTaskExecutor responsible for running the container's consumer threads [1][2][3]. Ownership and Lifecycle Management: 1. Default Executor: If no listenerTaskExecutor is provided, the container automatically creates a SimpleAsyncTaskExecutor on the fly [4]. This default executor is managed by the container instance itself [4]. 2. Custom Executor: If you provide a custom executor, ownership remains with the caller (e.g., an application-defined bean). The listener container does not shut down or close a custom-provided executor when the container stops [5]. The container simply stops submitting tasks to it; you are responsible for managing the lifecycle of your custom thread pool (e.g., shutting it down as part of the application context's closing process) [5]. Shutdown Behavior: - The listener container's stop process is governed by the ContainerProperties.shutdownTimeout (default 10,000 ms) [6][1][3]. - When the container stops, it signals the consumer thread to stop polling [5]. The container will wait up to the shutdownTimeout for the consumer thread to finish its current processing (e.g., finishing the current record/batch) before completing the stop operation [6][1][5]. - Because the default SimpleAsyncTaskExecutor (when created by the container) does not inherently wait for task termination by default, the container's stop logic relies on its own internal coordination to ensure a graceful exit within the configured timeout [5]. If using a custom executor, ensure it is configured to handle graceful shutdowns if task completion is required after the container stops [5].

Citations:


🌐 Web query:

Spring Kafka ContainerProperties setListenerTaskExecutor javadoc consumer thread task executor

💡 Result:

In Spring for Apache Kafka, the setListenerTaskExecutor method on the ContainerProperties class is used to define a custom TaskExecutor that manages the threads responsible for polling the Kafka consumer [1][2]. Key points regarding the listener task executor: Purpose: It specifies the executor used to run the threads that perform the Kafka polling loop [1][2]. A dedicated task executor is essential for managing thread allocation, especially in concurrent scenarios [3][4]. Default Behavior: If no custom executor is provided, the container uses a default SimpleAsyncTaskExecutor [3][5]. This default executor generates thread names based on the container bean name (typically following the pattern -C-n, or -m-C-n for concurrent containers) [3][4]. Configuration: When using a ConcurrentMessageListenerContainer, the provided executor is utilized to spawn a thread for each consumer instance (defined by the concurrency setting) [4][6]. If you use a pooled task executor, you must ensure it has sufficient threads available to handle the total concurrency across all containers using it [4]. Virtual Threads: You can set a listener task executor configured for virtual threads to improve performance and resource utilization [7]. Modern Spring Boot versions can often handle this automatically via the spring.threads.virtual.enabled property, but you can also configure it programmatically using setListenerTaskExecutor [7]. Note on Property Names: In some older versions or documentation references, you may see this referred to as the consumer executor [4][8]. The method setListenerTaskExecutor was introduced (notably since version 2.8.9) to provide a clear, explicit way to manage these polling threads [2].

Citations:


Avoid creating an unmanaged fixed pool here. KafkaMessageListenerContainer still uses a single consumer loop, so maxPoolSize won’t add record-processing parallelism. The local ExecutorService is never shut down; make it a managed bean or close it when the container stops.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterConsumerConfig.java`
around lines 163 - 166, Update the customExecutorEnabled branch in
PersisterConsumerConfig so the fixed-thread-pool ExecutorService is
lifecycle-managed: either expose it as a managed bean or register shutdown when
the KafkaMessageListenerContainer stops. Do not create an unmanaged local
executor, and preserve the existing listener task-executor configuration.

Comment on lines +70 to +73
Object a = message.get("attempts");
// A dead-letter envelope with no numeric attempts (a foreign/raw record) is treated as
// already at the ceiling, so it parks instead of being granted a fresh retry budget.
attempts = (a instanceof Number) ? ((Number) a).intValue() : maxRetries;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject invalid DLQ attempt counters.

Number.intValue() can turn negative or overflowing envelope values into counters below maxRetries, producing an effectively unbounded requeue loop. Treat any non-integral or out-of-range value as already at the retry ceiling.

Proposed fix
 Object a = message.get("attempts");
-attempts = (a instanceof Number) ? ((Number) a).intValue() : maxRetries;
+if (a instanceof Number number
+        && number.longValue() >= 0
+        && number.longValue() <= maxRetries
+        && number.doubleValue() == number.longValue()) {
+    attempts = Math.toIntExact(number.longValue());
+} else {
+    attempts = maxRetries;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Object a = message.get("attempts");
// A dead-letter envelope with no numeric attempts (a foreign/raw record) is treated as
// already at the ceiling, so it parks instead of being granted a fresh retry budget.
attempts = (a instanceof Number) ? ((Number) a).intValue() : maxRetries;
Object a = message.get("attempts");
// A dead-letter envelope with no numeric attempts (a foreign/raw record) is treated as
// already at the ceiling, so it parks instead of being granted a fresh retry budget.
if (a instanceof Number number
&& number.longValue() >= 0
&& number.longValue() <= maxRetries
&& number.doubleValue() == number.longValue()) {
attempts = Math.toIntExact(number.longValue());
} else {
attempts = maxRetries;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterMessageListener.java`
around lines 70 - 73, Update the attempts parsing in PersisterMessageListener so
only integral numeric values within the valid retry-counter range are accepted;
treat negative, overflowing, non-integral, and non-numeric values as maxRetries.
Avoid Number.intValue() silently narrowing invalid values, while preserving the
existing ceiling behavior for foreign or raw DLQ records.

Comment on lines +149 to +169
for (String record : records) {
try {
persistService.persist(topic, record);
persisted++;
} catch (Exception e) {
DbExceptionClassifier.Kind kind = DbExceptionClassifier.classify(e);
if (kind == DbExceptionClassifier.Kind.BENIGN) {
duplicates++;
} else if (kind == DbExceptionClassifier.Kind.TRANSIENT) {
log.warn("Transient failure during record-level isolation for topic {} - retrying whole message in place", topic, e);
throw new TransientPersistException("Transient DB failure isolating record for topic " + topic, e);
} else if (!fromDlq) {
sendToDlq(topic, record, 1, e);
deadLettered++;
} else if (attempts < maxRetries) {
sendToDlq(topic, record, attempts + 1, e);
deadLettered++;
} else {
sendToParking(topic, record, attempts, e);
parked++;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Defer DLQ publication until the isolation sweep succeeds without transient errors.

If an early record is dead-lettered and a later record fails transiently, Line 159 retries the original message, causing the early record to be published to the DLQ again on every retry. Stage permanent failures and publish them only after the complete sweep encounters no transient failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterMessageListener.java`
around lines 149 - 169, The record-isolation loop in PersisterMessageListener
must defer DLQ and parking publication until the entire sweep completes without
a transient failure. Stage each permanent failure and its retry metadata during
the loop, preserve benign/transient handling, then publish the staged entries
only after the loop succeeds, updating deadLettered and parked counts at
publication time.

Comment on lines +62 to +71
// Permanent / poison (incl. undeserialisable records): park a self-describing envelope (source
// topic + error reason), not the bare value, so a terminally-parked record can be triaged/replayed.
// Blocking send -> throws on failure, so a failed park is not committed (DefaultErrorHandler
// re-seeks instead of dropping the record).
Map<String, Object> parked = new HashMap<>();
parked.put("source", record.topic());
parked.put("body", record.value());
parked.put("error", ex == null ? null : ex.toString());
parked.put("ts", System.currentTimeMillis());
customKafkaTemplate.send(parkingTopic, parked);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and nearby symbols first
ast-grep outline core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterProducerConfig.java --view expanded || true

# Read the relevant section with line numbers
sed -n '1,220p' core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterProducerConfig.java | cat -n

# Find ErrorHandlingDeserializer and recoverer usage in the module
rg -n "ErrorHandlingDeserializer|DefaultErrorHandler|DeadLetterPublishingRecoverer|parkingTopic|customKafkaTemplate|Deserializer" core-services/egov-persister/src/main/java core-services/egov-persister/src/main/resources

Repository: egovernments/Digit-Core

Length of output: 10238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterConsumerConfig.java | cat -n
printf '\n---\n'
sed -n '1,240p' core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterBatchConsumerConfig.java | cat -n
printf '\n---\n'
sed -n '1,260p' core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterMessageListener.java | cat -n

Repository: egovernments/Digit-Core

Length of output: 30822


Preserve raw bytes in the parking envelope
PersisterProducerConfig.java:66-71 parks record.value(), which is null for deserialization failures under ErrorHandlingDeserializer. Copy the raw bytes from the exception header into body (or otherwise restore them) so poison records can be replayed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/consumer/PersisterProducerConfig.java`
around lines 62 - 71, Update the parking-envelope construction in the producer
error-handling flow to populate parked.body with the original raw bytes from the
deserialization exception header when record.value() is null, while preserving
the existing value for successfully deserialized records. Use the available
exception/header metadata associated with the failed record so poison messages
remain replayable.

Comment on lines +74 to +88
// Step 1: Parse all documents and pair with their applicable mappings
// Using LinkedHashMap to preserve message order
Map<Object, List<Mapping>> documentToMappings = new LinkedHashMap<>();

for (String json : jsons) {
Object document = Configuration.defaultConfiguration().jsonProvider().parse(json);
applicableMappings.put(document, filterMappings(map.get(topic), document));
documentToMappings.put(document, filterMappings(map.get(topic), document));
}

applicableMappings.forEach((jsonObj, mappings) -> {
// Step 2: Group documents by mapping (using mapping name as key for identity)
// This handles the case where different messages might have different versions
Map<String, List<Object>> mappingNameToDocuments = new LinkedHashMap<>();
Map<String, Mapping> mappingNameToMapping = new LinkedHashMap<>();

for (Map.Entry<Object, List<Mapping>> entry : documentToMappings.entrySet()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Prevent silent deduplication of identical JSON messages.

Using the parsed Object document as a key in a LinkedHashMap relies on its content-based equals() method. If a batch contains multiple identical JSON messages, subsequent messages will overwrite the first one, silently dropping them from the batch. Use a List<Map.Entry> instead to preserve all documents and maintain the exact multiplicity of the batch.

🐛 Proposed fix to prevent deduplication
-		// Step 1: Parse all documents and pair with their applicable mappings
-		// Using LinkedHashMap to preserve message order
-		Map<Object, List<Mapping>> documentToMappings = new LinkedHashMap<>();
-
-		for (String json : jsons) {
-			Object document = Configuration.defaultConfiguration().jsonProvider().parse(json);
-			documentToMappings.put(document, filterMappings(map.get(topic), document));
-		}
-
-		// Step 2: Group documents by mapping (using mapping name as key for identity)
-		// This handles the case where different messages might have different versions
-		Map<String, List<Object>> mappingNameToDocuments = new LinkedHashMap<>();
-		Map<String, Mapping> mappingNameToMapping = new LinkedHashMap<>();
-
-		for (Map.Entry<Object, List<Mapping>> entry : documentToMappings.entrySet()) {
+		// Step 1: Parse all documents and pair with their applicable mappings
+		// Using a List to preserve message order and avoid deduplicating identical JSON payloads
+		List<java.util.Map.Entry<Object, List<Mapping>>> documentToMappings = new ArrayList<>();
+
+		for (String json : jsons) {
+			Object document = Configuration.defaultConfiguration().jsonProvider().parse(json);
+			documentToMappings.add(new java.util.AbstractMap.SimpleEntry<>(document, filterMappings(map.get(topic), document)));
+		}
+
+		// Step 2: Group documents by mapping (using mapping name as key for identity)
+		// This handles the case where different messages might have different versions
+		Map<String, List<Object>> mappingNameToDocuments = new LinkedHashMap<>();
+		Map<String, Mapping> mappingNameToMapping = new LinkedHashMap<>();
+
+		for (java.util.Map.Entry<Object, List<Mapping>> entry : documentToMappings) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Step 1: Parse all documents and pair with their applicable mappings
// Using LinkedHashMap to preserve message order
Map<Object, List<Mapping>> documentToMappings = new LinkedHashMap<>();
for (String json : jsons) {
Object document = Configuration.defaultConfiguration().jsonProvider().parse(json);
applicableMappings.put(document, filterMappings(map.get(topic), document));
documentToMappings.put(document, filterMappings(map.get(topic), document));
}
applicableMappings.forEach((jsonObj, mappings) -> {
// Step 2: Group documents by mapping (using mapping name as key for identity)
// This handles the case where different messages might have different versions
Map<String, List<Object>> mappingNameToDocuments = new LinkedHashMap<>();
Map<String, Mapping> mappingNameToMapping = new LinkedHashMap<>();
for (Map.Entry<Object, List<Mapping>> entry : documentToMappings.entrySet()) {
// Step 1: Parse all documents and pair with their applicable mappings
// Using a List to preserve message order and avoid deduplicating identical JSON payloads
List<java.util.Map.Entry<Object, List<Mapping>>> documentToMappings = new ArrayList<>();
for (String json : jsons) {
Object document = Configuration.defaultConfiguration().jsonProvider().parse(json);
documentToMappings.add(new java.util.AbstractMap.SimpleEntry<>(document, filterMappings(map.get(topic), document)));
}
// Step 2: Group documents by mapping (using mapping name as key for identity)
// This handles the case where different messages might have different versions
Map<String, List<Object>> mappingNameToDocuments = new LinkedHashMap<>();
Map<String, Mapping> mappingNameToMapping = new LinkedHashMap<>();
for (java.util.Map.Entry<Object, List<Mapping>> entry : documentToMappings) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/service/PersistService.java`
around lines 74 - 88, Replace the documentToMappings LinkedHashMap in the
persist flow with a list of document-to-mappings entries so identical parsed
JSON documents remain separate batch items. Update the subsequent iteration to
process each entry in insertion order while preserving every message’s
applicable mappings and exact multiplicity.

Comment on lines +118 to 129
for (Object document : documents) {
try {
List<Object[]> rows = persistRepository.getRows(jsonMaps, document, basePath);
if (rows.isEmpty()) {
skippedDocuments++;
}
aggregatedRows.addAll(rows);
} catch (Exception e) {
skippedDocuments++;
log.warn("Failed to extract rows for basePath '{}': {}", basePath, e.getMessage());
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Remove the try-catch block to ensure malformed records are routed to the DLQ.

Swallowing exceptions from getRows prevents extraction failures (such as parsing errors or unexpected structural mismatches) from propagating. If a document fails extraction due to bad data, the batch will succeed without it, and the listener will commit the offset without ever sending the bad document to the dead-letter queue.

Remove the try-catch block so extraction errors bubble up, triggering the upstream listener's record-level isolation and DLQ routing.

🐛 Proposed fix to propagate extraction failures
 				for (Object document : documents) {
-					try {
-						List<Object[]> rows = persistRepository.getRows(jsonMaps, document, basePath);
-						if (rows.isEmpty()) {
-							skippedDocuments++;
-						}
-						aggregatedRows.addAll(rows);
-					} catch (Exception e) {
-						skippedDocuments++;
-						log.warn("Failed to extract rows for basePath '{}': {}", basePath, e.getMessage());
-					}
+					List<Object[]> rows = persistRepository.getRows(jsonMaps, document, basePath);
+					if (rows.isEmpty()) {
+						skippedDocuments++;
+					}
+					aggregatedRows.addAll(rows);
 				}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (Object document : documents) {
try {
List<Object[]> rows = persistRepository.getRows(jsonMaps, document, basePath);
if (rows.isEmpty()) {
skippedDocuments++;
}
aggregatedRows.addAll(rows);
} catch (Exception e) {
skippedDocuments++;
log.warn("Failed to extract rows for basePath '{}': {}", basePath, e.getMessage());
}
}
for (Object document : documents) {
List<Object[]> rows = persistRepository.getRows(jsonMaps, document, basePath);
if (rows.isEmpty()) {
skippedDocuments++;
}
aggregatedRows.addAll(rows);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/egov-persister/src/main/java/org/egov/infra/persist/service/PersistService.java`
around lines 118 - 129, Remove the try-catch around persistRepository.getRows in
the document-processing loop so extraction exceptions propagate to the upstream
listener for record-level isolation and DLQ routing. Keep the existing
skippedDocuments increment for empty results, and do not increment it or
log-and-continue when getRows fails.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core-services/boundary-service/README.md (1)

56-65: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Clarify the parent-ordering contract.

Lines 56-60 require batches to use already-persisted parents and recommend top-down creation, but line 65 says ordering across levels is not required. Clarify that Kafka ordering is unnecessary only when callers retry PARENT_NOT_FOUND; parent-before-child persistence is still required for successful validation.

Proposed wording
- so at-least-once redelivery and caller re-submission are safe no-ops. Bulk messages are keyed by the batch's shared parent code, so sibling batches under one parent stay ordered on the same partition.
+ so at-least-once redelivery and caller resubmission are safe no-ops. Parent-before-child submission is still required; Kafka partition ordering across hierarchy levels is not relied upon because callers retry `PARENT_NOT_FOUND`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core-services/boundary-service/README.md` around lines 56 - 65, Clarify the
README’s parent-ordering contract by reconciling the top-down batching guidance
with the statement that ordering across levels is not required: state that Kafka
or global submission ordering is unnecessary only when callers retry
PARENT_NOT_FOUND, while each child batch still requires its parent to be
persisted before validation succeeds.
core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/KafkaTemplateLoggingInterceptors.java (1)

42-58: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clear MDC after processing each record to prevent context leakage.

In the ConsumerInterceptor.onConsume method, TracerKafkaMdcUtil.applyMdcFromRecord sets the MDC context for the current thread (the Kafka consumer poll thread). Because the context is not cleared at the end of the loop, the correlationId and tenantId of the last record in the batch will leak and remain on the poll thread. This can result in incorrect correlation IDs appearing in internal Kafka logs or other interceptors executing on this thread.

Add a call to TracerKafkaMdcUtil.clearMdc() at the end of the loop to ensure the thread's MDC is left clean.

♻️ Proposed fix
             } else {
                 log.info(RECEIVED_MESSAGE, consumerRecord.topic(), consumerRecord.topic(), consumerRecord.key());
             }
+            TracerKafkaMdcUtil.clearMdc();
         }
         return consumerRecords;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/KafkaTemplateLoggingInterceptors.java`
around lines 42 - 58, Update the record-processing loop in
ConsumerInterceptor.onConsume to call TracerKafkaMdcUtil.clearMdc() after each
record’s logging completes, ensuring correlationId and tenantId do not remain on
the Kafka poll thread. Keep the existing MDC application and logging behavior
unchanged.
🧹 Nitpick comments (1)
core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/TracerKafkaMdcUtil.java (1)

67-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Support raw JSON parsing in correlationIdFromBody.

If consumerRecord.value() is a raw JSON String or byte[] (e.g., when the listener is configured with a StringDeserializer or ByteArrayDeserializer), objectMapper.convertValue will throw an IllegalArgumentException rather than parsing the JSON. This results in the correlation ID extraction silently failing and returning null.

To safely handle both parsed POJOs and raw JSON payload formats, add explicit parsing paths for strings and byte arrays before falling back to convertValue.

♻️ Proposed refactor
     `@SuppressWarnings`("unchecked")
     private static String correlationIdFromBody(Object value) {
         try {
-            Map<String, Object> requestMap = objectMapper.convertValue(value, Map.class);
+            Map<String, Object> requestMap;
+            if (value instanceof String) {
+                requestMap = objectMapper.readValue((String) value, Map.class);
+            } else if (value instanceof byte[]) {
+                requestMap = objectMapper.readValue((byte[]) value, Map.class);
+            } else {
+                requestMap = objectMapper.convertValue(value, Map.class);
+            }
             Object requestInfo = requestMap.containsKey(REQUEST_INFO_FIELD_NAME_IN_JAVA_CLASS_CASE)
                     ? requestMap.get(REQUEST_INFO_FIELD_NAME_IN_JAVA_CLASS_CASE)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/TracerKafkaMdcUtil.java`
around lines 67 - 81, Update correlationIdFromBody to explicitly parse String
and byte[] values as JSON before the existing objectMapper.convertValue
fallback, while preserving the current request-info and correlation-ID
extraction behavior for parsed objects. Ensure both raw payload formats are
handled without silently returning null due to convertValue rejecting them.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/TracerKafkaMdcUtil.java`:
- Around line 13-16: Replace the removed static
org.springframework.util.StringUtils.isEmpty import and update all affected
checks in TracerKafkaMdcUtil to use the Spring 6-compatible StringUtils.hasText
or hasLength semantics, preserving each existing empty-value behavior across the
referenced methods.

---

Outside diff comments:
In `@core-services/boundary-service/README.md`:
- Around line 56-65: Clarify the README’s parent-ordering contract by
reconciling the top-down batching guidance with the statement that ordering
across levels is not required: state that Kafka or global submission ordering is
unnecessary only when callers retry PARENT_NOT_FOUND, while each child batch
still requires its parent to be persisted before validation succeeds.

In
`@core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/KafkaTemplateLoggingInterceptors.java`:
- Around line 42-58: Update the record-processing loop in
ConsumerInterceptor.onConsume to call TracerKafkaMdcUtil.clearMdc() after each
record’s logging completes, ensuring correlationId and tenantId do not remain on
the Kafka poll thread. Keep the existing MDC application and logging behavior
unchanged.

---

Nitpick comments:
In
`@core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/TracerKafkaMdcUtil.java`:
- Around line 67-81: Update correlationIdFromBody to explicitly parse String and
byte[] values as JSON before the existing objectMapper.convertValue fallback,
while preserving the current request-info and correlation-ID extraction behavior
for parsed objects. Ensure both raw payload formats are handled without silently
returning null due to convertValue rejecting them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d3348a3-3a92-46fb-bafa-25347d780652

📥 Commits

Reviewing files that changed from the base of the PR and between 7c8d3ea and b242c9b.

📒 Files selected for processing (14)
  • core-services/boundary-service/CHANGELOG.md
  • core-services/boundary-service/README.md
  • core-services/boundary-service/pom.xml
  • core-services/egov-persister/CHANGELOG.md
  • core-services/egov-persister/README.md
  • core-services/egov-persister/pom.xml
  • core-services/libraries/tracer/CHANGELOG.md
  • core-services/libraries/tracer/pom.xml
  • core-services/libraries/tracer/readme.md
  • core-services/libraries/tracer/src/main/java/org/egov/tracer/config/TracerConfiguration.java
  • core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/KafkaTemplateLoggingInterceptors.java
  • core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/MdcRecordInterceptor.java
  • core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/TracerKafkaMdcUtil.java
  • core-services/libraries/tracer/src/main/resources/tracer.properties

Comment on lines +13 to +16
import static java.util.Objects.isNull;
import static org.egov.tracer.constants.TracerConstants.*;
import static org.springframework.util.StringUtils.isEmpty;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix compilation error: StringUtils.isEmpty is removed in Spring 6.

The project uses Spring Boot 3.4.5 (Spring Framework 6+), where org.springframework.util.StringUtils.isEmpty has been completely removed after being deprecated in Spring 5.3. This will cause a compilation failure.

Replace isEmpty with !StringUtils.hasText or !StringUtils.hasLength (or ObjectUtils.isEmpty).

🛠 Proposed fix
 import static java.util.Objects.isNull;
 import static org.egov.tracer.constants.TracerConstants.*;
-import static org.springframework.util.StringUtils.isEmpty;
+import org.springframework.util.StringUtils;

 /** Carries correlationId + tenantId across Kafka via record headers, and rebuilds MDC on consume. */

Update the method calls:

     public static void applyMdcFromRecord(ConsumerRecord<?, ?> consumerRecord) {
         String correlationId = headerValue(consumerRecord, CORRELATION_ID_HEADER);
-        if (isEmpty(correlationId))
+        if (!StringUtils.hasLength(correlationId))
             correlationId = correlationIdFromBody(consumerRecord.value());
         setOrRemove(CORRELATION_ID_MDC, correlationId);
     private static void addHeaderIfAbsent(ProducerRecord<?, ?> producerRecord, String headerName, String value) {
-        if (!isEmpty(value) && producerRecord.headers().lastHeader(headerName) == null)
+        if (StringUtils.hasLength(value) && producerRecord.headers().lastHeader(headerName) == null)
             producerRecord.headers().add(headerName, value.getBytes(StandardCharsets.UTF_8));
     }
     private static void setOrRemove(String key, String value) {
-        if (isEmpty(value))
+        if (!StringUtils.hasLength(value))
             MDC.remove(key);
         else
             MDC.put(key, value);
     }

Also applies to: 32-40, 48-51, 60-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core-services/libraries/tracer/src/main/java/org/egov/tracer/kafka/TracerKafkaMdcUtil.java`
around lines 13 - 16, Replace the removed static
org.springframework.util.StringUtils.isEmpty import and update all affected
checks in TracerKafkaMdcUtil to use the Spring 6-compatible StringUtils.hasText
or hasLength semantics, preserving each existing empty-value behavior across the
referenced methods.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants