nathanmarz/storm · error · RuntimeException

TopologyContext.registerMetric can only be called from…

Error message

TopologyContext.registerMetric can only be called from within overridden IBolt::prepare() or ISpout::open() method.

What it means

TopologyContext.registerMetric must be called during IBolt.prepare() or ISpout.open(); Storm tracks via an AtomicBoolean (_openOrPrepareWasCalled) when those lifecycle methods complete. Calling registerMetric afterwards (e.g. in execute/nextTuple) throws this RuntimeException because metrics must be registered before the metrics polling infrastructure starts consuming them.

Solutions

  1. Move all registerMetric calls into the body of prepare() (bolt) or open() (spout), before it returns.
  2. If metrics depend on async setup, register placeholder metrics in prepare and update their state later instead of registering late.
  3. If background threads need metrics, create and register the IMetric instance in prepare, then hand the instance to the thread.
  4. Use a different metrics mechanism (e.g. own reporter) for anything that must be initialized after startup.

Example fix

// before
public void execute(Tuple tuple) {
    if (counters == null) {
        counters = context.registerMetric("counters", new CountMetric(), 10); // throws
    }
}

// after
public void prepare(Map conf, TopologyContext context, OutputCollector collector) {
    counters = context.registerMetric("counters", new CountMetric(), 10);
}
public void execute(Tuple tuple) { counters.incr(); }
Defensive patterns

Strategy: validation

Validate before calling

// before calling registerMetric outside lifecycle:
// ensure you're inside prepare()/open(); if not, defer registration
boolean inLifecycle = Thread.currentThread().getName().startsWith("main");

Try / catch

try {
    context.registerMetric(name, metric, interval);
} catch (RuntimeException e) {
    if (e.getMessage().contains("registerMetric can only be called")) {
        LOG.warn("metric {} registered too late; skipping", name);
    } else { throw e; }
}

Prevention

When it happens

Trigger: Calling topologyContext.registerMetric(...) outside prepare/open — most commonly in execute(), nextTuple(), cleanup(), or an executor thread spawned by prepare after prepare returned.

Common situations: Developers lazily registering metrics on first tuple processed; registering metrics in a background thread started from prepare (the flag is already true by the time the thread registers); migrating metrics code from prepare into execute during refactors.

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 nathanmarz/storm@cdb116e942 (2026-09-12). Data as JSON: /api/errors/9fcac9d46d4b25a7. Report an issue: GitHub.

Appendix: source

Thrown at storm-core/src/jvm/backtype/storm/task/TopologyContext.java:230

    public void addTaskHook(ITaskHook hook) {
        hook.prepare(_stormConf, this);
        _hooks.add(hook);
    }
    
    public Collection<ITaskHook> getHooks() {
        return _hooks;
    }

    /*
     * Register a IMetric instance. 
     * Storm will then call getValueAndReset on the metric every timeBucketSizeInSecs
     * and the returned value is sent to all metrics consumers.
     * You must call this during IBolt::prepare or ISpout::open.
     * @return The IMetric argument unchanged.
     */
    public <T extends IMetric> T registerMetric(String name, T metric, int timeBucketSizeInSecs) {
        if((Boolean)_openOrPrepareWasCalled.deref() == true) {
            throw new RuntimeException("TopologyContext.registerMetric can only be called from within overridden " + 
                                       "IBolt::prepare() or ISpout::open() method.");
        }
        
        Map m1 = _registeredMetrics;
        if(!m1.containsKey(timeBucketSizeInSecs)) {
            m1.put(timeBucketSizeInSecs, new HashMap());
        }

        Map m2 = (Map)m1.get(timeBucketSizeInSecs);
        if(!m2.containsKey(_taskId)) {
            m2.put(_taskId, new HashMap());
        }

        Map m3 = (Map)m2.get(_taskId);
        if(m3.containsKey(name)) {
            throw new RuntimeException("The same metric name `" + name + "` was registered twice." );
        } else {
            m3.put(name, metric);

View on GitHub (pinned to cdb116e942)