Add batch-llm module: 100,000 synthetic tickets classified by a ChatClient in a Spring Batch 6 chunk step with Resilience4j rate limiting and retry, skip-and-record for malformed JSON, restart after kill -9 in a second JVM, and a per-chunk cost ledger. The model is a deterministic simulated ChatModel; prices are configurable placeholders. No provider called.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b4VQVEJWetTT1dYzyJ
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package com.ankurm.batchllm;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.springframework.batch.infrastructure.item.Chunk;
|
||||
import org.springframework.batch.infrastructure.item.ItemWriter;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
/**
|
||||
* Writes the results AND the cost ledger row for the chunk in one transaction, so a ledger row
|
||||
* exists if and only if its tickets do. The reader position, the results and the ledger either
|
||||
* all commit or all roll back.
|
||||
*/
|
||||
@Component
|
||||
public class ClassificationWriter implements ItemWriter<Classification> {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final UsageMeter meter;
|
||||
private final Pricing pricing;
|
||||
private final AtomicLong committedLedgerRows = new AtomicLong();
|
||||
|
||||
public ClassificationWriter(JdbcTemplate jdbc, UsageMeter meter, Pricing pricing) {
|
||||
this.jdbc = jdbc;
|
||||
this.meter = meter;
|
||||
this.pricing = pricing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(Chunk<? extends Classification> chunk) {
|
||||
List<? extends Classification> items = chunk.getItems();
|
||||
jdbc.batchUpdate("INSERT INTO TICKET_RESULT (TICKET_ID, CATEGORY, PRIORITY, CONFIDENCE) VALUES (?,?,?,?)",
|
||||
items, items.size(), (ps, c) -> {
|
||||
ps.setLong(1, c.ticketId());
|
||||
ps.setString(2, c.category());
|
||||
ps.setInt(3, c.priority());
|
||||
ps.setDouble(4, c.confidence());
|
||||
});
|
||||
|
||||
// A ticket skipped in an earlier, uncommitted attempt (the skip listener commits on its own) and
|
||||
// classified now must not stay in the skipped table.
|
||||
jdbc.batchUpdate("DELETE FROM SKIPPED_TICKET WHERE TICKET_ID = ?", items, items.size(),
|
||||
(ps, c) -> ps.setLong(1, c.ticketId()));
|
||||
|
||||
long first = items.stream().mapToLong(Classification::ticketId).min().orElse(0);
|
||||
long last = items.stream().mapToLong(Classification::ticketId).max().orElse(0);
|
||||
UsageMeter.Snapshot spent = meter.drain();
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCompletion(int status) {
|
||||
if (status == STATUS_COMMITTED) {
|
||||
long rows = committedLedgerRows.incrementAndGet();
|
||||
if (rows % 50 == 0) {
|
||||
System.out.println("PROGRESS committed-chunks-this-jvm=" + rows + " last-ticket=" + last);
|
||||
}
|
||||
} else {
|
||||
meter.restore(spent); // rolled back: the money was still spent
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
BigDecimal cost = pricing.cost(spent.promptTokens(), spent.completionTokens());
|
||||
jdbc.update("""
|
||||
INSERT INTO COST_LEDGER (FIRST_TICKET, LAST_TICKET, TICKETS_WRITTEN, TICKETS_SKIPPED, ATTEMPTS,
|
||||
REJECTED_429, UNAVAILABLE_503, RETRIES, BILLED_CALLS, BAD_REPLIES, PROMPT_TOKENS, COMPLETION_TOKENS,
|
||||
COST, PRICE_VERSION) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
first, last, items.size(), spent.skipped(), spent.attempts(), spent.rejected429(),
|
||||
spent.unavailable503(), spent.retries(), spent.billedCalls(), spent.badReplies(),
|
||||
spent.promptTokens(), spent.completionTokens(), cost, pricing.version());
|
||||
}
|
||||
|
||||
public long committedLedgerRows() {
|
||||
return committedLedgerRows.get();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user