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
- Move all registerMetric calls into the body of prepare() (bolt) or open() (spout), before it returns.
- If metrics depend on async setup, register placeholder metrics in prepare and update their state later instead of registering late.
- If background threads need metrics, create and register the IMetric instance in prepare, then hand the instance to the thread.
- 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
- Register all metrics at the top of prepare()/open().
- Never register metrics in execute()/nextTuple() or background threads.
- Pass pre-registered metric instances to worker threads instead of registering there.
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
- Could not instantiate a class listed in config under section
- A single worker should have 1 SystemBolt instance.
- Non-system tuples should never be sent to __system bolt.
- MeanReducer::reduce called with unsupported input type
- Fatal: could not find this task id in this component
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)