Skip to content

[Feat] analysis async를 MQ worker 구조로 전환하고 worker 상태 메타데이터를 확장 (#110)#116

Merged
shinae1023 merged 5 commits into
devfrom
feat/#110-MQ
Jul 4, 2026
Merged

[Feat] analysis async를 MQ worker 구조로 전환하고 worker 상태 메타데이터를 확장 (#110)#116
shinae1023 merged 5 commits into
devfrom
feat/#110-MQ

Conversation

@shinae1023

@shinae1023 shinae1023 commented Jul 4, 2026

Copy link
Copy Markdown
Member

✨ 어떤 이유로 PR를 하셨나요?

  • feature 병합
  • 버그 수정(아래에 issue #를 남겨주세요)
  • 코드 개선
  • 코드 수정
  • 배포
  • 기타(아래에 자세한 내용 기입해주세요)

📋 세부 내용 - 왜 해당 PR이 필요한지 작업 내용을 자세하게 설명해주세요

  • analysis 비동기 실행을 기존 @async 호출 대신 RabbitMQ publish 기반으로 전환했습니다.
  • FastAPI worker 연동을 위한 /api/internal/worker/analysis/* 내부 API와 analysis 전용 메시지/DTO를 추가했습니다.
  • analysis_async_tasks에 failureReason, workerId, retryCount, maxRetryCount, submittedAt, lastAttemptAt, queueLatencyMillis를 저장하도록 확장했습니다.

📸 작업 화면 스크린샷

⚠️ PR하기 전에 확인해주세요

  • 로컬테스트를 진행하셨나요?
  • 머지할 브랜치를 확인하셨나요?
  • 관련 label을 선택하셨나요?

🚨 관련 이슈 번호 [#110]

Summary by CodeRabbit

  • New Features

    • Added separate analysis worker RabbitMQ queue/routing configuration with configurable max retry count.
    • Extended async status responses to include worker identity, retry progress, failure reasons, queue latency, and attempt timestamps.
    • Introduced internal analysis-worker endpoints to retrieve context and update task lifecycle.
    • Added shared worker exchange support and correlation-confirm publishing with mandatory delivery.
  • Bug Fixes

    • Improved async retry/failure/completion handling with richer failure details.
    • Added/extended timeout handling to auto-fail overdue tasks with queue/worker timeout reasons.
  • Documentation / Chores / Tests

    • Updated environment examples and applied DB migrations for analysis async task metadata.
    • Added an integration test covering message publishing behavior.

@shinae1023 shinae1023 self-assigned this Jul 4, 2026
@shinae1023 shinae1023 added the ✨ feat New feature or request label Jul 4, 2026
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@shinae1023, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e8cecf53-16ee-43ce-acbd-f99a936ba196

📥 Commits

Reviewing files that changed from the base of the PR and between 18b5eb9 and ba9788f.

📒 Files selected for processing (13)
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisWorkerInternalController.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerCompleteRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerFailureRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerRetryRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/AnalysisAsyncTask.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncTaskService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisQueueProperties.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisTaskMessagePublisher.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingQueueProperties.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingTaskMessagePublisher.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeServiceTest.java
📝 Walkthrough

Walkthrough

Introduces RabbitMQ-backed async worker flows for analysis and job posting, with richer task metadata, internal worker endpoints, scoped task lookup, and queue/publisher configuration updates. Also adds payment confirmation timeout handling that can roll a processing payment back to pending.

Changes

Async worker pipeline

Layer / File(s) Summary
Task metadata and worker DTOs
src/main/java/.../entity/AnalysisAsyncTask.java, src/main/java/.../entity/JobPostingAsyncTask.java, src/main/java/.../dto/worker/*, src/main/java/.../dto/response/*, ops/db/migrations/*worker_metadata.sql
Adds retry, worker, queue-latency, submission, and attempt metadata to async task entities and responses, plus the matching worker request/response/message DTOs and DB columns.
Task lifecycle and status mapping
src/main/java/.../service/AnalysisAsyncTaskService.java, src/main/java/.../service/JobPostingAsyncTaskService.java, src/main/java/.../service/AnalysisAsyncFacadeService.java, src/main/java/.../service/JobPostingAsyncFacadeService.java, src/main/java/.../service/JobPostingAsyncProcessor.java, src/test/java/.../AnalysisAsyncFacadeServiceTest.java, src/test/java/.../JobPostingAsyncFacadeServiceTest.java, src/test/java/.../JobPostingAsyncTaskServiceTest.java
Updates pending creation, running/retry/failure transitions, timeout expiration, user-scoped task lookup, and richer status mapping, with tests adjusted for the new task shape and processor arguments.
Worker bridge and internal endpoints
src/main/java/.../service/AnalysisWorkerBridgeService.java, src/main/java/.../service/JobPostingWorkerBridgeService.java, src/main/java/.../controller/AnalysisWorkerInternalController.java, src/main/java/.../controller/JobPostingWorkerInternalController.java
Adds internal worker controllers and bridge services for running, retrying, failing, context retrieval, and completion flows, including internal API key validation and credit state handling.
RabbitMQ and environment wiring
src/main/java/.../config/RabbitMqConfig.java, src/main/java/.../service/AnalysisTaskMessagePublisher.java, src/main/java/.../service/JobPostingTaskMessagePublisher.java, src/main/java/.../service/AnalysisAsyncProcessor.java, src/main/java/.../service/AnalysisAsyncFacadeService.java, src/main/java/.../global/mq/service/RabbitPublishSupport.java, .env.example, .env.production.example, src/main/resources/application-dev.yaml, src/main/resources/application-prod.yaml, src/test/java/.../RabbitPublishSupportIntegrationTest.java
Adds shared worker exchange and analysis queue/DLQ bindings, publisher-confirm publishing support, message publishers, and environment/config values for job-posting and analysis routing and retry settings.

Payment timeout rollback

Layer / File(s) Summary
Payment rollback on timeout
src/main/java/.../entity/Payment.java, src/main/java/.../service/PaymentService.java, src/main/java/.../service/PaymentTransactionService.java, src/main/java/.../service/TossPaymentClient.java, src/test/java/.../PaymentServiceTest.java
Adds pending-state reset on payment, maps Toss access failures to external timeout errors, and rolls back a processing confirmation to pending when that timeout occurs.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AsyncFacadeService
  participant AsyncTaskService
  participant AsyncProcessor
  participant RabbitPublishSupport
  participant RabbitMQ
  participant Worker
  participant WorkerInternalController
  participant WorkerBridgeService

  Client->>AsyncFacadeService: submit(user, request)
  AsyncFacadeService->>AsyncTaskService: createPendingTask(userId, maxRetryCount)
  AsyncFacadeService->>AsyncProcessor: process(taskId, userId, mockApplyId, maxRetryCount)
  AsyncProcessor->>RabbitPublishSupport: publish(task message)
  RabbitPublishSupport->>RabbitMQ: convertAndSend + confirm wait
  RabbitMQ-->>Worker: deliver task message
  Worker->>WorkerInternalController: markRunning / getContext / completeTask
  WorkerInternalController->>WorkerBridgeService: delegate validated request
  WorkerBridgeService->>AsyncTaskService: update task state or fetch status
Loading
sequenceDiagram
  participant Client
  participant PaymentService
  participant TossPaymentClient
  participant PaymentTransactionService
  participant Payment

  Client->>PaymentService: confirm(...)
  PaymentService->>TossPaymentClient: confirm(...)
  TossPaymentClient-->>PaymentService: GeneralException(EXTERNAL_SERVICE_TIMEOUT)
  PaymentService->>PaymentTransactionService: rollbackConfirmationToPending(...)
  PaymentTransactionService->>Payment: resetToPending()
  PaymentService-->>Client: rethrow GeneralException
Loading

Possibly related PRs

Suggested labels: ♻️ refactor

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly reflects the main change: moving analysis async to an MQ worker flow and expanding worker metadata.
Description check ✅ Passed The description covers the required why/details sections and issue number, with only the optional screenshot/checklist items left sparse.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#110-MQ

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: 7

🧹 Nitpick comments (4)
src/main/java/com/jobdri/jobdri_api/global/config/RabbitMqConfig.java (1)

52-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Analysis queue/DLQ/binding beans duplicate the job-posting pattern verbatim.

analysisQueue, analysisDeadLetterQueue, and analysisBinding mirror jobPostingIngestQueue/jobPostingIngestDeadLetterQueue/jobPostingIngestBinding structurally. As more worker types are added (per the PR stack outline), this duplication will keep growing. Consider extracting a small factory helper (e.g., buildDurableQueueWithDlq(name, dlqName)) to construct queue+DLQ pairs consistently.

🤖 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 `@src/main/java/com/jobdri/jobdri_api/global/config/RabbitMqConfig.java` around
lines 52 - 77, The analysis queue, dead-letter queue, and binding setup in
RabbitMqConfig duplicates the job-posting worker pattern, so refactor the
repeated queue/binding creation into a shared helper or factory method. Use the
existing bean methods such as analysisQueue, analysisDeadLetterQueue, and
analysisBinding alongside the jobPostingIngest* equivalents to centralize
durable queue-with-DLQ construction and reuse the same binding setup for future
worker types.
src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisWorkerInternalController.java (1)

36-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Repeated API-key validation boilerplate across every endpoint.

Each handler manually calls internalApiKeyValidator.validate(internalApiKey) first. Centralizing this via a HandlerInterceptor/filter scoped to /api/internal/worker/** would remove the duplication and eliminate the risk of a future endpoint being added without the check.

🤖 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
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisWorkerInternalController.java`
around lines 36 - 104, The Internal API key check is duplicated in every handler
of AnalysisWorkerInternalController, which makes it easy to miss on future
endpoints. Move the validation out of markRunning, markRetry, failTask,
getContext, completeTask, and getTask into a centralized HandlerInterceptor or
filter scoped to the internal worker routes, and keep the controller methods
focused on request handling only.
src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisTaskMessagePublisher.java (2)

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

Constructor vs. field injection mismatch.

rabbitTemplate is constructor-injected via Lombok's @RequiredArgsConstructor, while exchange/routingKey rely on @Value field injection on non-final fields. This is harmless in production (Spring populates both), but complicates unit testing — instantiating via the generated constructor leaves exchange/routingKey null unless a full Spring context is used or values are set via reflection.

♻️ Optional refactor: constructor-inject the values
-    private final RabbitTemplate rabbitTemplate;
-
-    `@Value`("${app.worker.analysis.exchange:jobdri.worker.exchange}")
-    private String exchange;
-
-    `@Value`("${app.worker.analysis.routing-key:analysis.execute}")
-    private String routingKey;
+    private final RabbitTemplate rabbitTemplate;
+
+    private final String exchange;
+    private final String routingKey;
+
+    public AnalysisTaskMessagePublisher(
+            RabbitTemplate rabbitTemplate,
+            `@Value`("${app.worker.analysis.exchange:jobdri.worker.exchange}") String exchange,
+            `@Value`("${app.worker.analysis.routing-key:analysis.execute}") String routingKey
+    ) {
+        this.rabbitTemplate = rabbitTemplate;
+        this.exchange = exchange;
+        this.routingKey = routingKey;
+    }
🤖 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
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisTaskMessagePublisher.java`
around lines 12 - 20, The AnalysisTaskMessagePublisher setup mixes constructor
injection for rabbitTemplate with field injection for exchange and routingKey,
which leaves those values unset when using the Lombok-generated constructor in
tests. Refactor the class to inject exchange and routingKey through the
constructor as well, aligning them with rabbitTemplate in the
AnalysisTaskMessagePublisher constructor/@RequiredArgsConstructor usage so the
publisher can be instantiated cleanly without Spring field injection or
reflection.

22-30: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

No publisher confirms/returns — message loss is silent.

By default, Spring AMQP publishing is fire-and-forget: "Publishing messages is an asynchronous mechanism and, by default, messages that cannot be routed are dropped by RabbitMQ." If the exchange/queue binding is misconfigured or the broker nacks the message, publish() returns normally with no error, leaving the task stuck (no worker will ever pick it up) with nothing surfacing the failure.

Consider enabling publisher confirms/returns on the connection factory (via RabbitMqConfig, layer 6) and wiring a ConfirmCallback/ReturnsCallback here to detect and react to failed publishes (e.g., mark the task failed).

🤖 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
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisTaskMessagePublisher.java`
around lines 22 - 30, Add publish-failure handling to
AnalysisTaskMessagePublisher.publish so silent RabbitMQ drops/nacks are
detected. Wire publisher confirms and returns through RabbitMqConfig/connection
factory, then register a ConfirmCallback and ReturnsCallback on rabbitTemplate
to catch broker nacks or unroutable messages. In publish(), use the existing
exchange/routingKey flow but ensure failed sends are surfaced and can trigger
task-failure handling instead of returning normally.
🤖 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
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerCompleteRequest.java`:
- Around line 12-14: The `AnalysisWorkerCompleteRequest` still carries
`openAiRequestId` even though it is not used in the success flow. Either remove
`openAiRequestId` from `AnalysisWorkerCompleteRequest` if it should not be
retained, or thread it through `AnalysisAsyncTask` and the completion path so it
is persisted alongside `workerId` and `queueLatencyMillis`.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerFailureRequest.java`:
- Around line 8-15: The failure request already carries observability fields,
but `failTask(...)` and `markFailed` are not persisting them, so failure records
lose attribution data. Update the failure path to accept and forward
`AnalysisWorkerFailureRequest.workerId`, `queueLatencyMillis`, and
`openAiRequestId` into `failTask(...)` and then through `markFailed`, ensuring
the failure entity/logging captures these values instead of dropping them.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/AnalysisAsyncTask.java`:
- Around line 129-145: `markRetryScheduled` in `AnalysisAsyncTask` does not
enforce the stored `maxRetryCount`, so retries can be rescheduled indefinitely.
Update `markRetryScheduled(FailureReason, String, int)` to compare the incoming
`retryCount` against `maxRetryCount` and, when the cap is reached or exceeded,
transition the task to `FAILED` instead of `PENDING` (reuse `markFailed`/the
same status/message fields as appropriate). Keep the retry-state logic
self-contained in `AnalysisAsyncTask` so callers like `AnalysisAsyncTaskService`
and `AnalysisWorkerBridgeService` cannot accidentally bypass the limit.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncProcessor.java`:
- Around line 13-16: The publish path in AnalysisAsyncProcessor.process can fail
after credit has already been reserved, leaving the user charged even though no
task was queued. Update the flow around
analysisTaskMessagePublisher.publish(...) to catch synchronous publish failures
and trigger the same credit release/refund logic used elsewhere in
createAndProcessTask, using the existing AnalysisAsyncProcessor and task/credit
service methods to keep the reservation state consistent.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java`:
- Around line 33-39: `markRunning` and `markRetry` in
`AnalysisWorkerBridgeService` currently delegate straight to
`AnalysisAsyncTaskService` and can reopen finished tasks by calling
`markRunning` or `markRetryScheduled` without checking terminal states. Add the
same terminal-state protection used by `failTask` before delegating, so tasks
already in `SUCCEEDED` or `FAILED` are not moved back to `RUNNING` or `PENDING`.
Update the guard in the bridge service and ensure the corresponding
`AnalysisAsyncTask` transition methods respect the finished-state check
consistently.
- Around line 79-100: `completeTask` currently trusts `request.userId()` and
`request.mockApplyId()` instead of verifying them against the persisted
`AnalysisAsyncTask` identity. Add the same user/mockApply equality guard used in
`getContext` near the start of `completeTask`, before
`prepareAnalysisExecution`, `finalizeAnalysis`, and `confirmAnalysisCredit`, and
fail fast with the existing error pattern if they do not match. Use
`AnalysisWorkerBridgeService`, `getTask`, and `completeTask` to locate the flow
and ensure the task’s stored user/mockApply pair is the only one allowed to
complete it.

In `@src/main/java/com/jobdri/jobdri_api/global/config/RabbitMqConfig.java`:
- Around line 18-23: The worker exchange is configured separately from the
publishers, which can cause `AnalysisTaskMessagePublisher` and
`JobPostingTaskMessagePublisher` to send messages to exchanges that
`RabbitMqConfig.workerExchange` does not bind to. Update the publisher wiring to
inject and reuse the shared `workerExchange` bean (or its resolved name) instead
of reading independent publisher exchange properties, so both publishers always
target the same exchange as the queue bindings.

---

Nitpick comments:
In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisWorkerInternalController.java`:
- Around line 36-104: The Internal API key check is duplicated in every handler
of AnalysisWorkerInternalController, which makes it easy to miss on future
endpoints. Move the validation out of markRunning, markRetry, failTask,
getContext, completeTask, and getTask into a centralized HandlerInterceptor or
filter scoped to the internal worker routes, and keep the controller methods
focused on request handling only.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisTaskMessagePublisher.java`:
- Around line 12-20: The AnalysisTaskMessagePublisher setup mixes constructor
injection for rabbitTemplate with field injection for exchange and routingKey,
which leaves those values unset when using the Lombok-generated constructor in
tests. Refactor the class to inject exchange and routingKey through the
constructor as well, aligning them with rabbitTemplate in the
AnalysisTaskMessagePublisher constructor/@RequiredArgsConstructor usage so the
publisher can be instantiated cleanly without Spring field injection or
reflection.
- Around line 22-30: Add publish-failure handling to
AnalysisTaskMessagePublisher.publish so silent RabbitMQ drops/nacks are
detected. Wire publisher confirms and returns through RabbitMqConfig/connection
factory, then register a ConfirmCallback and ReturnsCallback on rabbitTemplate
to catch broker nacks or unroutable messages. In publish(), use the existing
exchange/routingKey flow but ensure failed sends are surfaced and can trigger
task-failure handling instead of returning normally.

In `@src/main/java/com/jobdri/jobdri_api/global/config/RabbitMqConfig.java`:
- Around line 52-77: The analysis queue, dead-letter queue, and binding setup in
RabbitMqConfig duplicates the job-posting worker pattern, so refactor the
repeated queue/binding creation into a shared helper or factory method. Use the
existing bean methods such as analysisQueue, analysisDeadLetterQueue, and
analysisBinding alongside the jobPostingIngest* equivalents to centralize
durable queue-with-DLQ construction and reuse the same binding setup for future
worker types.
🪄 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 Plus

Run ID: 6dca01d3-e631-4e16-9c2c-1319a67ef89b

📥 Commits

Reviewing files that changed from the base of the PR and between bde20f9 and e5309fa.

📒 Files selected for processing (22)
  • .env.example
  • .env.production.example
  • ops/db/migrations/20260704_analysis_async_tasks_worker_metadata.sql
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisWorkerInternalController.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/AnalysisAsyncStatusResponse.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisTaskMessage.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerCompleteRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerContextRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerContextResponse.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerFailureRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerRetryRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerRunningRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/AnalysisAsyncTask.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncProcessor.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncTaskService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisTaskMessagePublisher.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java
  • src/main/java/com/jobdri/jobdri_api/global/config/RabbitMqConfig.java
  • src/main/resources/application-dev.yaml
  • src/main/resources/application-prod.yaml
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeServiceTest.java

Comment on lines +8 to +15
public record AnalysisWorkerFailureRequest(
@NotBlank String errorMessage,
@NotNull FailureReason failureReason,
@Min(0) int retryCount,
@NotBlank String workerId,
Long queueLatencyMillis,
String openAiRequestId
) {

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the bridge/task service's failTask/markFailed persist worker metadata anywhere else
rg -n -A10 'void failTask' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java
rg -n -A10 'markFailed' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncTaskService.java

Repository: JobDri-Developer/BackEnd

Length of output: 1171


Pass the telemetry fields through the failure path. workerId, queueLatencyMillis, and openAiRequestId are validated on AnalysisWorkerFailureRequest but dropped by failTask(...), so failure records never get those observability fields. Thread them into failTask/markFailed so failures can be attributed.

🤖 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
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisWorkerFailureRequest.java`
around lines 8 - 15, The failure request already carries observability fields,
but `failTask(...)` and `markFailed` are not persisting them, so failure records
lose attribution data. Update the failure path to accept and forward
`AnalysisWorkerFailureRequest.workerId`, `queueLatencyMillis`, and
`openAiRequestId` into `failTask(...)` and then through `markFailed`, ensuring
the failure entity/logging captures these values instead of dropping them.

Comment on lines +13 to +16
public void process(String taskId, Long userId, Long mockApplyId, String creditReferenceId, int maxRetryCount) {
analysisTaskMessagePublisher.publish(
AnalysisTaskMessage.of(taskId, userId, mockApplyId, creditReferenceId, maxRetryCount)
);

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
# Inspect full facade catch block for credit release handling
rg -n -B5 -A20 'catch \(RuntimeException e\)' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeService.java

Repository: JobDri-Developer/BackEnd

Length of output: 1200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Facade =="
sed -n '1,140p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeService.java

echo
echo "== Processor =="
sed -n '1,120p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncProcessor.java

echo
echo "== Search for credit release/refund paths =="
rg -n "release|refund|reserveAnalysisCredit|deleteTask\\(" src/main/java/com/jobdri/jobdri_api/domain/analysis -S

Repository: JobDri-Developer/BackEnd

Length of output: 8551


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AnalysisAsyncTaskService =="
sed -n '1,140p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncTaskService.java

echo
echo "== AnalysisService =="
sed -n '1,160p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java

echo
echo "== AnalysisWorkerBridgeService (credit release path) =="
sed -n '1,180p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java

Repository: JobDri-Developer/BackEnd

Length of output: 19024


Release reserved credit when message publish fails. createAndProcessTask() only deletes the task in its catch, so a synchronous failure from analysisTaskMessagePublisher.publish(...) after reserveAnalysisCredit(...) leaves the user's credit consumed even though no job was queued. Releasing/refunding the credit here would close that gap.

🤖 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
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncProcessor.java`
around lines 13 - 16, The publish path in AnalysisAsyncProcessor.process can
fail after credit has already been reserved, leaving the user charged even
though no task was queued. Update the flow around
analysisTaskMessagePublisher.publish(...) to catch synchronous publish failures
and trigger the same credit release/refund logic used elsewhere in
createAndProcessTask, using the existing AnalysisAsyncProcessor and task/credit
service methods to keep the reservation state consistent.

Comment on lines 18 to 23
@Bean
public DirectExchange workerExchange(
@Value("${app.worker.job-posting.exchange:jobdri.worker.exchange}") String exchangeName
@Value("${app.worker.exchange:jobdri.worker.exchange}") String exchangeName
) {
return new DirectExchange(exchangeName, true, false);
}

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

printf 'Repo root: '; pwd
echo

# Locate relevant files
fd -a 'RabbitMqConfig.java|AnalysisTaskMessagePublisher.java|JobPostingIngestMessagePublisher.java|application.yml|application.yaml|application-*.yml|application-*.yaml' .

# Show AST outlines for the likely files if present
for f in src/main/java/com/jobdri/jobdri_api/global/config/RabbitMqConfig.java \
         src/main/java/com/jobdri/jobdri_api/global/messaging/AnalysisTaskMessagePublisher.java \
         src/main/java/com/jobdri/jobdri_api/global/messaging/JobPostingIngestMessagePublisher.java \
         src/main/resources/application.yml \
         src/main/resources/application.yaml; do
  if [ -f "$f" ]; then
    echo "=== OUTLINE: $f ==="
    ast-grep outline "$f" --view expanded || true
    echo
  fi
done

# Print line-numbered snippets for relevant Java files
for f in src/main/java/com/jobdri/jobdri_api/global/config/RabbitMqConfig.java \
         src/main/java/com/jobdri/jobdri_api/global/messaging/AnalysisTaskMessagePublisher.java \
         src/main/java/com/jobdri/jobdri_api/global/messaging/JobPostingIngestMessagePublisher.java; do
  if [ -f "$f" ]; then
    echo "=== FILE: $f ==="
    wc -l "$f"
    cat -n "$f" | sed -n '1,220p'
    echo
  fi
done

# Print relevant app config snippets
for f in src/main/resources/application.yml src/main/resources/application.yaml; do
  if [ -f "$f" ]; then
    echo "=== FILE: $f ==="
    wc -l "$f"
    cat -n "$f" | sed -n '1,260p'
    echo
  fi
done

Repository: JobDri-Developer/BackEnd

Length of output: 5138


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Quick focused search for the exchange properties and bindings.
rg -n --hidden --no-ignore-vcs \
  'app\.worker(\.analysis|\.job-posting|\.exchange)|workerExchange|analysisBinding|jobPostingIngestBinding|AnalysisTaskMessagePublisher|JobPostingIngestMessagePublisher' \
  src/main/java src/main/resources

Repository: JobDri-Developer/BackEnd

Length of output: 3745


Use the shared worker exchange for both publishers. AnalysisTaskMessagePublisher and JobPostingTaskMessagePublisher can publish to app.worker.analysis.exchange / app.worker.job-posting.exchange, while RabbitMqConfig binds both queues to workerExchange. If either publisher is overridden independently, messages go to an exchange the queues are not bound to and worker tasks are dropped. Inject workerExchange directly, or reuse its resolved name, instead of keeping separate per-publisher exchange properties.

🤖 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 `@src/main/java/com/jobdri/jobdri_api/global/config/RabbitMqConfig.java` around
lines 18 - 23, The worker exchange is configured separately from the publishers,
which can cause `AnalysisTaskMessagePublisher` and
`JobPostingTaskMessagePublisher` to send messages to exchanges that
`RabbitMqConfig.workerExchange` does not bind to. Update the publisher wiring to
inject and reuse the shared `workerExchange` bean (or its resolved name) instead
of reading independent publisher exchange properties, so both publishers always
target the same exchange as the queue bindings.

@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: 3

🧹 Nitpick comments (1)
src/main/resources/application-prod.yaml (1)

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

Optional: analysis block omits queue/processing timeout overrides.

Unlike job-posting, the analysis block exposes only max-retry-count. If the analysis timeout policy is meant to be operator-tunable like job-posting, consider adding queue-timeout-minutes/processing-timeout-minutes env-backed keys for parity; otherwise it silently relies on hardcoded @Value defaults.

🤖 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 `@src/main/resources/application-prod.yaml` around lines 93 - 98, The analysis
configuration block currently exposes only max-retry-count and leaves timeout
behavior hidden behind default `@Value` values. Update the analysis section in
application-prod.yaml to match the job-posting pattern by adding env-backed
queue-timeout-minutes and processing-timeout-minutes keys alongside the existing
analysis.exchange, analysis.queue, analysis.routing-key, analysis.dlq, and
analysis.max-retry-count entries so operators can override them consistently.
🤖 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
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java`:
- Around line 110-117: `markRetryScheduled` in `JobPostingAsyncTask` should
refresh queue metadata when a task is re-queued: update `submittedAt` to the
current time so `expireTimedOutTaskIfNeeded()` uses the new enqueue time, and
clear attempt-scoped fields such as `workerId` and `startedAt` alongside the
existing status/message/failure updates. Keep the retry reset logic in
`markRetryScheduled` consistent with the task lifecycle so a retried pending
task is treated as a fresh queue entry.

In
`@src/main/java/com/jobdri/jobdri_api/global/mq/service/RabbitPublishSupport.java`:
- Around line 26-64: The `RabbitPublishSupport.publish()` flow is blocking the
HTTP/request thread by waiting in `awaitPublisherConfirm`, which can exhaust the
servlet pool under load. Move publisher-confirm waiting off the request path (or
make it fully async) and ensure late confirms are handled idempotently so a
timeout/failure does not delete state that worker callbacks still need. Focus
the fix around `publish`, `awaitPublisherConfirm`, and the
`CorrelationData`/confirm handling so retries and callbacks do not produce
`*_ASYNC_TASK_NOT_FOUND` errors.

In
`@src/test/java/com/jobdri/jobdri_api/global/mq/service/RabbitPublishSupportIntegrationTest.java`:
- Around line 24-58: The RabbitPublishSupportIntegrationTest is still hard-wired
to a local RabbitMQ broker via its Spring test properties, but CI does not start
one. Update this test setup by either wiring RabbitMQ through a CI service
container or converting the test to use Testcontainers/conditional execution,
and keep the configuration anchored around RabbitPublishSupportIntegrationTest,
RabbitTemplate, RabbitAdmin, and the DirectExchange bean so it only runs when a
broker is available.

---

Nitpick comments:
In `@src/main/resources/application-prod.yaml`:
- Around line 93-98: The analysis configuration block currently exposes only
max-retry-count and leaves timeout behavior hidden behind default `@Value` values.
Update the analysis section in application-prod.yaml to match the job-posting
pattern by adding env-backed queue-timeout-minutes and
processing-timeout-minutes keys alongside the existing analysis.exchange,
analysis.queue, analysis.routing-key, analysis.dlq, and analysis.max-retry-count
entries so operators can override them consistently.
🪄 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 Plus

Run ID: 4d11173d-d3c1-440d-baff-3cbc8c704296

📥 Commits

Reviewing files that changed from the base of the PR and between e5309fa and 18b5eb9.

📒 Files selected for processing (36)
  • .gradle-local/wrapper/dists/gradle-8.14.4-bin/92wwslzcyst3phie3o264zltu/gradle-8.14.4-bin.zip.lck
  • .gradle-local/wrapper/dists/gradle-8.14.4-bin/92wwslzcyst3phie3o264zltu/gradle-8.14.4-bin.zip.part
  • ops/db/migrations/20260704_job_posting_async_tasks_user_id.sql
  • ops/db/migrations/20260704_job_posting_async_tasks_worker_metadata.sql
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisTaskMessage.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncProcessor.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisTaskMessagePublisher.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingWorkerInternalController.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingAsyncStatusResponse.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingIngestTaskMessage.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerFailureRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerRetryRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/worker/JobPostingWorkerRunningRequest.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/repository/JobPostingAsyncTaskRepository.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncFacadeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncProcessor.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingTaskMessagePublisher.java
  • src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingWorkerBridgeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/entity/Payment.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/PaymentTransactionService.java
  • src/main/java/com/jobdri/jobdri_api/domain/payment/service/TossPaymentClient.java
  • src/main/java/com/jobdri/jobdri_api/global/mq/service/RabbitPublishSupport.java
  • src/main/resources/application-dev.yaml
  • src/main/resources/application-prod.yaml
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncFacadeServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/global/mq/service/RabbitPublishSupportIntegrationTest.java
💤 Files with no reviewable changes (2)
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/worker/AnalysisTaskMessage.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeService.java
✅ Files skipped from review due to trivial changes (1)
  • src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisTaskMessagePublisher.java
  • src/main/resources/application-dev.yaml
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncProcessor.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java

Comment on lines +110 to +117
public void markRetryScheduled(FailureReason failureReason, String errorMessage, int retryCount) {
this.status = TaskStatus.PENDING;
this.message = "채용 공고 비동기 재시도를 대기 중입니다.";
this.failureReason = failureReason;
this.error = errorMessage;
this.retryCount = Math.max(0, retryCount);
this.completedAt = null;
}

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where markRetryScheduled is invoked and whether submittedAt is re-stamped on retry publish
rg -nP '\bmarkRetryScheduled\b' --type=java -C3
rg -nP 'submittedAt' --type=java -g '!**/test/**' -C2

Repository: JobDri-Developer/BackEnd

Length of output: 162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant classes before reading targeted sections
fd -a 'JobPostingAsyncTask.java|JobPostingAsyncTaskService.java|*AsyncTask*' src/main/java

# Locate retry/timeout-related methods and their call sites
rg -n --hidden --glob '!**/test/**' '\bmarkRetryScheduled\b|\bexpireTimedOutTaskIfNeeded\b|\bisExpired\(|\bsubmittedAt\b|\bworkerId\b|\bstartedAt\b' src/main/java

# Show the task entity around the retry method
sed -n '1,220p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java

# Show the service sections that evaluate queue timeout / retry handling
sed -n '1,260p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java

Repository: JobDri-Developer/BackEnd

Length of output: 567


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find exact relevant files in the repository
fd --type f --hidden 'JobPostingAsyncTask.java' src/main/java
fd --type f --hidden 'JobPostingAsyncTaskService.java' src/main/java

# Search for retry and timeout symbols across the main Java sources
rg -n --hidden --glob '!**/test/**' '\bmarkRetryScheduled\b|\bexpireTimedOutTaskIfNeeded\b|\bisExpired\(|\bsubmittedAt\b|\bworkerId\b|\bstartedAt\b' src/main/java

# Print the task entity around the retry method
sed -n '1,220p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java

# Print the service around timeout handling and retry publishing
sed -n '1,320p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java

Repository: JobDri-Developer/BackEnd

Length of output: 22947


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the exact repository paths for the relevant classes
fd --fixed-strings 'JobPostingAsyncTask.java' src/main/java
fd --fixed-strings 'JobPostingAsyncTaskService.java' src/main/java

# Search retry/timeout-related symbols in the main Java sources
rg -n --hidden --glob '!**/test/**' '\bmarkRetryScheduled\b|\bexpireTimedOutTaskIfNeeded\b|\bisExpired\(|\bsubmittedAt\b|\bworkerId\b|\bstartedAt\b' src/main/java

# Read the entity and service implementations in focused slices
sed -n '1,220p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java
sed -n '1,320p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncTaskService.java

Repository: JobDri-Developer/BackEnd

Length of output: 22947


Retry scheduling should refresh queue metadata

markRetryScheduled leaves submittedAt unchanged, but expireTimedOutTaskIfNeeded() expires PENDING tasks against that timestamp. A retried task can therefore be marked QUEUE_TIMEOUT before the next attempt starts. Refresh submittedAt here and clear attempt-scoped fields like workerId and startedAt when re-queuing.

🤖 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
`@src/main/java/com/jobdri/jobdri_api/domain/jobposting/entity/JobPostingAsyncTask.java`
around lines 110 - 117, `markRetryScheduled` in `JobPostingAsyncTask` should
refresh queue metadata when a task is re-queued: update `submittedAt` to the
current time so `expireTimedOutTaskIfNeeded()` uses the new enqueue time, and
clear attempt-scoped fields such as `workerId` and `startedAt` alongside the
existing status/message/failure updates. Keep the retry reset logic in
`markRetryScheduled` consistent with the task lifecycle so a retried pending
task is treated as a fresh queue entry.

Comment on lines +26 to +64
public void publish(
String exchange,
String routingKey,
Object payload,
String correlationId,
String failureMessage,
MessagePostProcessor messagePostProcessor
) {
CorrelationData correlationData = new CorrelationData(correlationId);
rabbitTemplate.convertAndSend(exchange, routingKey, payload, messagePostProcessor, correlationData);
awaitPublisherConfirm(correlationData, failureMessage);
}

private void awaitPublisherConfirm(CorrelationData correlationData, String failureMessage) {
try {
CorrelationData.Confirm confirm = correlationData.getFuture()
.get(publishConfirmTimeoutMillis, TimeUnit.MILLISECONDS);

if (confirm == null || !confirm.isAck()) {
throw new GeneralException(
GeneralErrorCode.SERVICE_UNAVAILABLE,
failureMessage + buildReasonSuffix(confirm != null ? confirm.getReason() : null)
);
}

ReturnedMessage returnedMessage = correlationData.getReturned();
if (returnedMessage != null) {
throw new GeneralException(
GeneralErrorCode.SERVICE_UNAVAILABLE,
failureMessage + " routingKey=" + returnedMessage.getRoutingKey()
);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new GeneralException(GeneralErrorCode.SERVICE_UNAVAILABLE, failureMessage);
} catch (ExecutionException | TimeoutException e) {
throw new GeneralException(GeneralErrorCode.SERVICE_UNAVAILABLE, failureMessage);
}
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files \
  src/main/java/com/jobdri/jobdri_api/global/mq/service/RabbitPublishSupport.java \
  src/main/java/com/jobdri/jobdri_api/global/mq/service \
  src/main/java/com/jobdri/jobdri_api/**/JobPostingTaskMessagePublisher.java \
  src/main/java/com/jobdri/jobdri_api/**/AnalysisTaskMessagePublisher.java \
  src/main/java/com/jobdri/jobdri_api/**/JobPostingAsyncFacadeService.java \
  src/main/java/com/jobdri/jobdri_api/**/AnalysisAsyncFacadeService.java \
  src/main/java/com/jobdri/jobdri_api/** \
  src/main/java/**/controller/** \
  src/main/java/**/internal/** \
  src/main/java/**/task/** \
  2>/dev/null | sed 's#^`#-` #'

printf '\n== Outline RabbitPublishSupport ==\n'
ast-grep outline src/main/java/com/jobdri/jobdri_api/global/mq/service/RabbitPublishSupport.java --view expanded || true

printf '\n== Outline likely callers ==\n'
for f in \
  $(git ls-files 'src/main/java/**/JobPostingTaskMessagePublisher.java' 'src/main/java/**/AnalysisTaskMessagePublisher.java' 'src/main/java/**/JobPostingAsyncFacadeService.java' 'src/main/java/**/AnalysisAsyncFacadeService.java' 2>/dev/null); do
  echo "--- $f"
  ast-grep outline "$f" --view expanded || true
done

printf '\n== Search for timeout / delete / task not found ==\n'
rg -n "publishConfirmTimeoutMillis|delete\\(|not found|TASK_NOT_FOUND|SERVICE_UNAVAILABLE|correlationData|getReturned\\(|awaitPublisherConfirm|convertAndSend\\(" src/main/java || true

Repository: JobDri-Developer/BackEnd

Length of output: 29932


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== RabbitPublishSupport =='
sed -n '1,220p' src/main/java/com/jobdri/jobdri_api/global/mq/service/RabbitPublishSupport.java

echo
echo '== JobPostingAsyncFacadeService =='
sed -n '1,240p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncFacadeService.java

echo
echo '== AnalysisAsyncFacadeService =='
sed -n '1,260p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncFacadeService.java

echo
echo '== JobPostingWorkerBridgeService =='
sed -n '1,220p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingWorkerBridgeService.java

echo
echo '== AnalysisWorkerBridgeService =='
sed -n '1,240p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisWorkerBridgeService.java

echo
echo '== Controllers mentioning async submit =='
rg -n "submit\\(" src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller src/main/java/com/jobdri/jobdri_api/domain/analysis/controller || true

Repository: JobDri-Developer/BackEnd

Length of output: 24203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== JobPostingAsyncProcessor =='
sed -n '1,240p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingAsyncProcessor.java

echo
echo '== AnalysisAsyncProcessor =='
sed -n '1,260p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAsyncProcessor.java

echo
echo '== JobPostingAiController submit endpoint =='
sed -n '1,180p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingAiController.java

echo
echo '== AnalysisController submit endpoint =='
sed -n '1,140p' src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisController.java

echo
echo '== Worker internal controllers =='
sed -n '1,220p' src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingWorkerInternalController.java
echo
sed -n '1,240p' src/main/java/com/jobdri/jobdri_api/domain/analysis/controller/AnalysisWorkerInternalController.java

Repository: JobDri-Developer/BackEnd

Length of output: 22418


Avoid waiting for publisher confirms on the HTTP thread

publish() blocks the request thread until a broker confirm arrives, so bursts of submissions can exhaust the servlet pool. The timeout path is also unsafe: a late confirm can still mean the message was accepted, but the current failure handling deletes the pending task, which leaves worker callbacks hitting *_ASYNC_TASK_NOT_FOUND and can trigger duplicate retries. Consider moving confirm handling off the request path or making late confirms/task state transitions idempotent.

🤖 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
`@src/main/java/com/jobdri/jobdri_api/global/mq/service/RabbitPublishSupport.java`
around lines 26 - 64, The `RabbitPublishSupport.publish()` flow is blocking the
HTTP/request thread by waiting in `awaitPublisherConfirm`, which can exhaust the
servlet pool under load. Move publisher-confirm waiting off the request path (or
make it fully async) and ensure late confirms are handled idempotently so a
timeout/failure does not delete state that worker callbacks still need. Focus
the fix around `publish`, `awaitPublisherConfirm`, and the
`CorrelationData`/confirm handling so retries and callbacks do not produce
`*_ASYNC_TASK_NOT_FOUND` errors.

Comment on lines +24 to +58
@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(properties = {
"spring.rabbitmq.host=localhost",
"spring.rabbitmq.port=5672",
"spring.rabbitmq.username=guest",
"spring.rabbitmq.password=guest",
"spring.rabbitmq.publisher-confirm-type=correlated",
"spring.rabbitmq.publisher-returns=true",
"spring.rabbitmq.template.mandatory=true"
})
class RabbitPublishSupportIntegrationTest {

@Autowired
private RabbitPublishSupport rabbitPublishSupport;

@Autowired
private RabbitTemplate rabbitTemplate;

@Autowired
private RabbitAdmin rabbitAdmin;

@Autowired
private DirectExchange workerExchange;

@Value("${app.worker.job-posting.queue:jobdri.job-posting.ingest}")
private String queueName;

@Value("${app.worker.job-posting.routing-key:job-posting.ingest}")
private String routingKey;

@BeforeEach
void purgeQueue() {
rabbitAdmin.purgeQueue(queueName, true);
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for broker provisioning / testcontainers / CI service config
rg -nP 'RabbitMQContainer|Testcontainers|`@EnabledIf`|rabbitmq' -g '!**/node_modules/**' -C2
fd -i -e yml -e yaml . .github 2>/dev/null | xargs -r rg -nl 'rabbitmq' 2>/dev/null

Repository: JobDri-Developer/BackEnd

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '## Relevant files'
git ls-files '.github/**' 'docker*' '*compose*' 'src/test/resources/**' 'src/main/resources/**' | sed -n '1,200p'

echo
echo '## RabbitMQ/Testcontainers/CI references'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/target/**' \
  'RabbitMQContainer|Testcontainers|`@EnabledIf`|`@DisabledIf`|spring\.rabbitmq|rabbitmq|localhost:5672|5672|guest/guest' \
  .github src/test/resources src/main/resources src/test/java . 2>/dev/null | sed -n '1,260p'

Repository: JobDri-Developer/BackEnd

Length of output: 4147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '## .github/workflows/ci.yml'
cat -n .github/workflows/ci.yml | sed -n '1,220p'

echo
echo '## src/test/resources/application-test.yaml'
cat -n src/test/resources/application-test.yaml | sed -n '1,220p'

echo
echo '## RabbitMQ-related main config'
cat -n src/main/resources/application.yaml | sed -n '1,220p'

Repository: JobDri-Developer/BackEnd

Length of output: 4746


Provision RabbitMQ for this integration test
src/test/java/com/jobdri/jobdri_api/global/mq/service/RabbitPublishSupportIntegrationTest.java still binds to localhost:5672, but .github/workflows/ci.yml only runs ./gradlew clean test and does not start RabbitMQ. Add a CI service container or switch this test to Testcontainers/conditional execution so builds don’t fail when no broker is available.

🤖 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
`@src/test/java/com/jobdri/jobdri_api/global/mq/service/RabbitPublishSupportIntegrationTest.java`
around lines 24 - 58, The RabbitPublishSupportIntegrationTest is still
hard-wired to a local RabbitMQ broker via its Spring test properties, but CI
does not start one. Update this test setup by either wiring RabbitMQ through a
CI service container or converting the test to use Testcontainers/conditional
execution, and keep the configuration anchored around
RabbitPublishSupportIntegrationTest, RabbitTemplate, RabbitAdmin, and the
DirectExchange bean so it only runs when a broker is available.

@shinae1023 shinae1023 merged commit a36909d into dev Jul 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant