StatefulTestEncoder.java

/*
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v2.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.core.encoder;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Test encoder with {@link #isStateful()} {@code true}, used to assert that an
 * appender serializes {@link #headerBytes()}, {@link #encode(Object)}, and
 * {@link #footerBytes()} and keeps them in that order.
 *
 * <p>Lifecycle is a three-state machine:</p>
 * <ul>
 *   <li>{@link Phase#INIT} ��� before the header has been written. {@code encode}
 *       in this phase sets {@link #hadEncodeBeforeHeader()}.</li>
 *   <li>{@link Phase#OPEN} ��� set at the <em>end</em> of {@code headerBytes()}, so
 *       an {@code encode} that sneaks in during the header still counts as
 *       before-header.</li>
 *   <li>{@link Phase#CLOSED} ��� set at the <em>start</em> of {@code footerBytes()},
 *       so an {@code encode} during the footer counts as after-footer
 *       ({@link #hadEncodeAfterFooter()}).</li>
 * </ul>
 *
 * <p>{@link #enter()} / {@link #leave()} wrap every public encoder method. If a
 * second call arrives while one is in flight, {@link #hadOverlap()} becomes
 * true. That is the signal that {@code streamWriteLock} failed to serialize
 * the stateful path.</p>
 *
 * <p>Output is a bracketed comma-separated list assembled by concatenating
 * writes: {@code "["} + first event + {@code ","} + later events + {@code "]"}.
 * Concurrent {@code encode} without a lock can omit commas or interleave
 * tokens, which the tests detect as a malformed list.</p>
 *
 * @param <E> event type
 */
public class StatefulTestEncoder<E> extends EncoderBase<E> {

    public static final String HEADER = "[";
    public static final String FOOTER = "]";

    /**
     * Encoder lifecycle as observed by {@link #encode(Object)}.
     */
    enum Phase {
        INIT, OPEN, CLOSED
    }

    /** Current lifecycle; written in header/footer, read in encode. */
    private volatile Phase phase = Phase.INIT;

    /**
     * When true, {@link #headerBytes()} and {@link #footerBytes()} sleep so
     * concurrent appends have a wider window to race start/stop.
     */
    private volatile boolean delayLifecycle;

    /** Count of nested/overlapping header, encode, or footer calls. */
    private final AtomicInteger inFlight = new AtomicInteger();
    private final AtomicInteger encodeCount = new AtomicInteger();
    private final AtomicBoolean overlap = new AtomicBoolean();
    private final AtomicBoolean encodeBeforeHeader = new AtomicBoolean();
    private final AtomicBoolean encodeAfterFooter = new AtomicBoolean();

    /**
     * Always {@code true} so {@code OutputStreamAppender} takes the stateful
     * lock protocol.
     */
    @Override
    public boolean isStateful() {
        return true;
    }

    /**
     * Enable a short sleep in header/footer to enlarge start/stop races.
     */
    public void setDelayLifecycle(boolean delayLifecycle) {
        this.delayLifecycle = delayLifecycle;
    }

    /** {@code true} if two encoder methods ran at the same time. */
    public boolean hadOverlap() {
        return overlap.get();
    }

    /** {@code true} if {@code encode} ran before {@code headerBytes} completed. */
    public boolean hadEncodeBeforeHeader() {
        return encodeBeforeHeader.get();
    }

    /** {@code true} if {@code encode} ran after {@code footerBytes} began. */
    public boolean hadEncodeAfterFooter() {
        return encodeAfterFooter.get();
    }

    /** Number of {@code encode} invocations, including those that violated order. */
    public int getEncodeCount() {
        return encodeCount.get();
    }

    /**
     * Writes {@link #HEADER} and only then marks the encoder {@link Phase#OPEN}.
     */
    @Override
    public byte[] headerBytes() {
        enter();
        try {
            maybeDelay();
            phase = Phase.OPEN;
            return HEADER.getBytes(StandardCharsets.UTF_8);
        } finally {
            leave();
        }
    }

    /**
     * Records order/overlap violations, then emits the event with a leading
     * comma except for the first event. {@link Thread#yield()} widens the
     * overlap window if the caller is not holding {@code streamWriteLock}.
     */
    @Override
    public byte[] encode(E event) {
        enter();
        try {
            Phase current = phase;
            if (current == Phase.INIT) {
                encodeBeforeHeader.set(true);
            } else if (current == Phase.CLOSED) {
                encodeAfterFooter.set(true);
            }
            Thread.yield();
            int n = encodeCount.getAndIncrement();
            String token = (n == 0 ? "" : ",") + event;
            return token.getBytes(StandardCharsets.UTF_8);
        } finally {
            leave();
        }
    }

    /**
     * Marks {@link Phase#CLOSED} first so a concurrent {@code encode} is
     * reported as after-footer, then writes {@link #FOOTER}.
     */
    @Override
    public byte[] footerBytes() {
        enter();
        try {
            phase = Phase.CLOSED;
            maybeDelay();
            return FOOTER.getBytes(StandardCharsets.UTF_8);
        } finally {
            leave();
        }
    }

    /**
     * Detect overlapping encoder calls: {@code getAndIncrement() != 0} means
     * another header/encode/footer is already running.
     */
    private void enter() {
        if (inFlight.getAndIncrement() != 0) {
            overlap.set(true);
        }
    }

    private void leave() {
        inFlight.decrementAndGet();
    }

    private void maybeDelay() {
        if (!delayLifecycle) {
            return;
        }
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}