OrderRecoveryJob.java

package com.ecommerce.trade.application.service;

import com.ecommerce.trade.application.exception.TradeException;
import com.ecommerce.trade.infrastructure.config.OrderProperties;
import com.ecommerce.trade.infrastructure.config.TradeSchedulingConfig;
import com.ecommerce.trade.infrastructure.observability.TradeOrderRecoveryObservability;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
@ConditionalOnProperty(prefix = "ecommerce.trade.order", name = "recovery-enabled", havingValue = "true")
public class OrderRecoveryJob {

    private static final Logger log = LoggerFactory.getLogger(OrderRecoveryJob.class);

    private final TradeOrderService orderService;
    private final OrderProperties properties;
    private final TradeOrderRecoveryObservability observability;
    private final String owner = "trade-order-recovery-"
            + UUID.randomUUID().toString().replace("-", "");

    public OrderRecoveryJob(
            TradeOrderService orderService,
            OrderProperties properties,
            TradeOrderRecoveryObservability observability) {
        this.orderService = orderService;
        this.properties = properties;
        this.observability = observability;
    }

    @Scheduled(
            fixedDelayString = "${ecommerce.trade.order.recovery-delay:5000}",
            scheduler = TradeSchedulingConfig.ORDER_RECOVERY_SCHEDULER)
    public void recoverOrders() {
        observability.observe(this::recoverOrdersOnce);
    }

    private void recoverOrdersOnce() {
        for (String orderNo : orderService.findRecoverableOrderNumbers(properties.recoveryBatchSize())) {
            if (!orderService.tryClaimRecovery(orderNo, owner)) {
                continue;
            }
            try {
                orderService.recoverOrder(orderNo);
            } catch (TradeException exception) {
                log.debug("Order recovery lost a state race: orderNo={}, code={}", orderNo, exception.error().code());
            } catch (RuntimeException exception) {
                log.warn("Order recovery failed: orderNo={}", orderNo, exception);
            } finally {
                orderService.releaseRecoveryClaim(orderNo, owner);
            }
        }
        for (String orderNo : orderService.findTimedOutOrderNumbers(properties.recoveryBatchSize())) {
            if (!orderService.tryClaimRecovery(orderNo, owner)) {
                continue;
            }
            try {
                orderService.cancelTimedOutOrder(orderNo);
            } catch (TradeException exception) {
                log.debug("Payment timeout lost a state race: orderNo={}, code={}", orderNo, exception.error().code());
            } catch (RuntimeException exception) {
                log.warn("Payment-timeout recovery failed: orderNo={}", orderNo, exception);
            } finally {
                orderService.releaseRecoveryClaim(orderNo, owner);
            }
        }
    }
}