apache/druid · error · IllegalStateException

Already started

Error message

Already started

What it means

MessageRelay's internal listener uses a watermark AtomicReference to ensure start() is idempotent: it CASes the watermark from INIT to 0. If the watermark is no longer INIT, the relay has already been started, and calling start() again throws ISE('Already started'). This protects against double registration of the server listener and duplicate message fetch loops.

Source

Thrown at server/src/main/java/org/apache/druid/messages/client/MessageRelay.java:109

  /**
   * Retrieves messages that are being sent to this client and hands them to {@link #listener}.
   */
  private class Collector
  {
    private final MessageListener<MessageType> listener;
    private final AtomicLong epoch = new AtomicLong(INIT);
    private final AtomicLong watermark = new AtomicLong(INIT);
    private final AtomicReference<ListenableFuture<?>> currentCall = new AtomicReference<>();

    public Collector(final MessageListener<MessageType> listener)
    {
      this.listener = listener;
    }

    private void start()
    {
      if (!watermark.compareAndSet(INIT, 0)) {
        throw new ISE("Already started");
      }

      listener.serverAdded(serverNode);
      issueNextGetMessagesCall();
    }

    private void issueNextGetMessagesCall()
    {
      if (closed.get()) {
        return;
      }

      final long theEpoch = epoch.get();
      final long theWatermark = watermark.get();

      log.debug(
          "Getting messages from server[%s] for client[%s] (current state: epoch[%s] watermark[%s]).",
          serverNode.getHostAndPortToUse(),

View on GitHub (pinned to 9b90983fd2)

Solutions

  1. Ensure start() is invoked exactly once per MessageRelay instance; recreate the relay object before restarting
  2. Guard the restart path so lifecycle stop() fully resets or replaces the relay before start() runs
  3. Serialize initialization with a single owner (e.g. lifecycle start/stop management) to avoid concurrent start calls

Example fix

// before
relay.setListener(listener);
relay.start(); // called again on restart -> ISE
// after
if (relay != null) { relay.stop(); }
relay = new MessageRelay(...);
relay.setListener(listener);
relay.start();
Defensive patterns

Strategy: try-catch

Try / catch

try { relay.start(); } catch (IllegalStateException e) { if (!e.getMessage().contains("Already started")) { throw e; } log.debug("MessageRelay already started, ignoring"); }

Prevention

When it happens

Trigger: Calling the private start() twice on the same MessageRelay instance — e.g. a lifecycle supervisor restart that does not recreate the relay object, or racing start() calls from two threads where the second loses the CAS.

Common situations: Server lifecycle restart/reconfiguration paths that re-invoke start without constructing a new MessageRelay; concurrent initialization during coordinator leadership transitions.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/5e0c295634be100f. Report an issue: GitHub.