apache/pulsar · error · IllegalArgumentException

Tenants differ

Error message

Tenants differ

What it means

SourceConfigUtils.validateUpdate enforces that a source update cannot change the tenant that owns the source. The existing stored config and the new config must have identical tenant values, otherwise the update is rejected.

Source

Thrown at pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/SourceConfigUtils.java:383

                validateSourceConfig(sourceConfig, sourceFunction);
            } else {
                log.warn("Skipping annotation based validation of sink config as classloading is disabled");
            }
        }

        return new ExtractedSourceDetails(sourceClassName, typeArg.asErasure().getTypeName());
    }

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

    public static SourceConfig validateUpdate(SourceConfig existingConfig, SourceConfig newConfig) {
        SourceConfig 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("Function Names differ");
        }
        if (!StringUtils.isEmpty(newConfig.getClassName())) {
            mergedConfig.setClassName(newConfig.getClassName());
        }
        if (!StringUtils.isEmpty(newConfig.getTopicName())) {
            mergedConfig.setTopicName(newConfig.getTopicName());
        }
        if (!StringUtils.isEmpty(newConfig.getSerdeClassName())) {
            mergedConfig.setSerdeClassName(newConfig.getSerdeClassName());
        }
        if (!StringUtils.isEmpty(newConfig.getSchemaType())) {
            mergedConfig.setSchemaType(newConfig.getSchemaType());

View on GitHub (pinned to 820761864e)

Solutions

  1. Resubmit the update with the tenant matching the existing source's tenant
  2. If the source genuinely must move tenants, delete it and create a new source under the target tenant
  3. Fix tooling/templates so they preserve the original tenant field on update

Example fix

// before
SourceConfig update = config; update.setTenant("tenantB");
// after
SourceConfig update = config; update.setTenant(existingConfig.getTenant()); // create a new source instead to move tenants
Defensive patterns

Strategy: validation

Validate before calling

if (!Objects.equals(existing.getTenant(), update.getTenant())) {
    throw new IllegalArgumentException("tenant is immutable on update");
}

Try / catch

try { SourceConfigUtils.validateUpdate(existing, update); } catch (IllegalArgumentException e) { if (e.getMessage().equals("Tenants differ")) { update.setTenant(existing.getTenant()); } else { throw e; } }

Prevention

When it happens

Trigger: Calling validateUpdate(existingConfig, newConfig) where existingConfig.getTenant() does not equal newConfig.getTenant() — typically via a source update API where the submitted config names a different tenant.

Common situations: Admin copying a source config into another tenant and submitting it as an update instead of creating a new source; typo in tenant name in the update request; tooling that regenerates the full config with defaults pointing to a different tenant.

Related errors


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