~/home of geeks

Batch Abarbeitung generalisieren

· 293 Wörter · 2 Minute(n) Lesedauer

red steam train travelling over arch bridge, in the style of swiss style, i cant believe how beautiful this is, biblical grandeur, cargopunk, bold colors, strong lines, pristine naturalism, konica big mini, romanesque, naturalistiv charm

Vor einiger Zeit musste ich größere Listen von Daten abarbeiten und die Ergebnisse zwischenspeichern. An mehreren Stellen im Code hatte ich dann Batch-Schleifen, die eine Liste in Batches á N Stück aufteilten und diese verarbeiteten. Die Gelegenheit, das etwas generischer zu Formulieren.

Die Grundstruktur einer Batch-weisen Verarbeitung einer Liste von Elementen sieht wie folgt aus:

List data = ...
final int batchSize = 50;
int fromOffset = 0;
do {
    int toOffset = Math.min(data.size(), fromOffset + batchSize);
    List batch = data.subList(fromOffset, toOffset);

    // verarbeite Batch

    fromOffset = toOffset;
} while (fromOffset < data.size());

Formuliert man das ganze mit einem Context-Objekt, in dem Informationen zum aktuellen Batch stehen, sowie ein Interface für Konsumenten, erhält man eine generische Batch-Abarbeitung:

@Log4j
public class Batch {
    public interface Consumer<T> {
        void consume(List<T> batch, Context context);
    }

    @Getter
    @AllArgsConstructor
    @Builder
    @EqualsAndHashCode
    public static class Context {
        private int currentBlockSize;
        private int blockSize;
        private int currentOffset;
    }

    public static <T> void process(List<T> data, int blockSize, Consumer<T> consumer) {
        final int maxOffset = data.size();
        int fromOffset = 0;
        do {
            int currentBlockSize = Math.min(blockSize, maxOffset - fromOffset);
            int toOffset = fromOffset + currentBlockSize;
            log.info("Verarbeite Block {} bis {} von {}", fromOffset, toOffset, maxOffset);
            consumer.consume(data.subList(fromOffset, toOffset),
                    Context.builder()
                            .currentOffset(fromOffset)
                            .currentBlockSize(currentBlockSize)
                            .blockSize(blockSize)
                            .build());
            fromOffset = toOffset;
        } while (fromOffset < maxOffset);
    }
}

Die Nutzung ist relativ einfach:

@Test
public void process() {
    List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    List<Integer> receivedBatchEntries = new ArrayList<>();
    List<Batch.Context> contexts = new ArrayList<>();
    Batch.process(ints, 3, (batch, context) -> {
        receivedBatchEntries.addAll(batch);
        contexts.add(context);
    });

    assertThat(receivedBatchEntries, contains(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
    assertThat(contexts, contains(
            Batch.Context.builder().blockSize(3).currentBlockSize(3).currentOffset(0).build(),
            Batch.Context.builder().blockSize(3).currentBlockSize(3).currentOffset(3).build(),
            Batch.Context.builder().blockSize(3).currentBlockSize(3).currentOffset(6).build(),
            // Letzter lauf hat nur ein Element
            Batch.Context.builder().blockSize(3).currentBlockSize(1).currentOffset(9).build()
    ));
}

Das Ganze kann man dann auch auf asynchrone und parallele Abarbeitungen erweitern.