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
- Resubmit the update with the tenant matching the existing source's tenant
- If the source genuinely must move tenants, delete it and create a new source under the target tenant
- 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
- Build update payloads from the existing config (clone + mutate) instead of from templates
- Never edit tenant/namespace/name fields in update requests
- To move a source, delete and recreate under the target tenant
- Compare submitted config against stored config in CI before issuing updates
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
- Tenants differ
- Namespaces differ
- Function Names differ
- Input Topics cannot be altered
- isRegexPattern for input topic
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/3617cad9043970ab.
Report an issue: GitHub.