apache/pulsar · error · IllegalArgumentException

Tenants differ

Error message

Tenants differ

What it means

Update-compatibility check in SinkConfigUtils.validateUpdate: the existing sink and the new sink configuration belong to different tenants, and a sink update cannot move a sink across tenants (updates must keep tenant/namespace/name identity); the diverging tenant fields are the faulty input.

Source

Thrown at pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/SinkConfigUtils.java:613

            retval.addAll(sinkConfig.getTopicToSchemaType().keySet());
        }
        if (sinkConfig.getInputSpecs() != null) {
            retval.addAll(sinkConfig.getInputSpecs().keySet());
        }
        return retval;
    }

    @SneakyThrows
    public static SinkConfig clone(SinkConfig sinkConfig) {
        return ObjectMapperFactory.getMapper().reader().readValue(
                ObjectMapperFactory.getMapper().writer().writeValueAsBytes(sinkConfig), SinkConfig.class);
    }

    public static SinkConfig validateUpdate(SinkConfig existingConfig, SinkConfig newConfig) {
        SinkConfig mergedConfig = clone(existingConfig);

        if (!existingConfig.getTenant().equals(newConfig.getTenant())) {
            throw new IllegalArgumentException("Tenants differ");
        }
        if (!existingConfig.getNamespace().equals(newConfig.getNamespace())) {
            throw new IllegalArgumentException("Namespaces differ");
        }
        if (!existingConfig.getName().equals(newConfig.getName())) {
            throw new IllegalArgumentException("Sink Names differ");
        }
        if (!StringUtils.isEmpty(newConfig.getClassName())) {
            mergedConfig.setClassName(newConfig.getClassName());
        }
        if (!StringUtils.isEmpty(newConfig.getSourceSubscriptionName()) && !newConfig.getSourceSubscriptionName()
                .equals(existingConfig.getSourceSubscriptionName())) {
            throw new IllegalArgumentException("Subscription Name cannot be altered");
        }

        if (newConfig.getInputSpecs() == null) {
            newConfig.setInputSpecs(new HashMap<>());
        }

View on GitHub (pinned to 820761864e)

Solutions

  1. Keep the tenant unchanged; create a new sink under the other tenant

Example fix

// before
newConfig.setTenant("tenant-b");
admin.sinks().updateSink(existingTenant, ns, name, newConfig);
// after
newConfig.setTenant(existingTenant);
admin.sinks().updateSink(existingTenant, ns, name, newConfig);
Defensive patterns

Strategy: validation

Validate before calling

if (!existing.getTenant().equals(newConfig.getTenant())) {
  throw new IllegalStateException("Update cannot change tenant; delete+create instead");
}

Try / catch

try {
  admin.sinks().updateSink(tenant, ns, name, newConfig);
} catch (IllegalArgumentException e) {
  if (e.getMessage() != null && e.getMessage().equals("Tenants differ")) {
    // create the sink under the new tenant instead
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling SinkConfigUtils.validateUpdate(existingConfig, newConfig) where newConfig.getTenant() differs from existingConfig.getTenant().

Common situations: Reusing an update request payload across tenants; programmatically cloning a sink config and changing tenant to 'move' the sink instead of delete+create.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/db8e4c6251ecb4a0. Report an issue: GitHub.