1515
1616package io .confluent .kafka .schemaregistry .storage ;
1717
18+ import static io .confluent .kafka .schemaregistry .utils .QualifiedSubject .CONTEXT_DELIMITER ;
19+ import static io .confluent .kafka .schemaregistry .utils .QualifiedSubject .CONTEXT_PREFIX ;
1820import static io .confluent .kafka .schemaregistry .utils .QualifiedSubject .DEFAULT_CONTEXT ;
1921
2022import com .google .common .annotations .VisibleForTesting ;
6466import java .util .ArrayList ;
6567import java .util .Collections ;
6668import java .util .Iterator ;
69+ import java .util .LinkedHashSet ;
6770import java .util .List ;
6871import java .util .Locale ;
6972import java .util .Map ;
@@ -994,13 +997,15 @@ private void forwardSetModeRequestToLeader(
994997
995998 private void forwardDeleteSubjectModeRequestToLeader (
996999 String subject ,
1000+ boolean recursive ,
9971001 Map <String , String > headerProperties )
9981002 throws SchemaRegistryRequestForwardingException {
9991003 UrlList baseUrl = leaderRestService .getBaseUrls ();
10001004
1001- log .debug ("Forwarding delete subject mode request {} to {}" , subject , baseUrl );
1005+ log .debug ("Forwarding delete subject mode request {} to {} with recursive={}" ,
1006+ subject , baseUrl , recursive );
10021007 try {
1003- leaderRestService .deleteSubjectMode (headerProperties , subject );
1008+ leaderRestService .deleteSubjectMode (headerProperties , subject , recursive );
10041009 } catch (IOException e ) {
10051010 throw new SchemaRegistryRequestForwardingException (
10061011 String .format (
@@ -1248,31 +1253,49 @@ public void setModeOrForward(String subject, ModeUpdateRequest mode, boolean for
12481253
12491254 public void deleteSubjectMode (String subject )
12501255 throws SchemaRegistryStoreException , OperationNotPermittedException {
1256+ deleteSubjectMode (subject , false );
1257+ }
1258+
1259+ public void deleteSubjectMode (String subject , boolean recursive )
1260+ throws SchemaRegistryStoreException , OperationNotPermittedException {
12511261 if (!allowModeChanges ) {
12521262 throw new OperationNotPermittedException ("Mode changes are not allowed" );
12531263 }
12541264 try {
12551265 kafkaStore .waitUntilKafkaReaderReachesLastOffset (subject , kafkaStoreTimeoutMs );
12561266 deleteMode (subject );
1267+
1268+ // If recursive and subject is a context, delete modes for all subjects under it
1269+ if (recursive && QualifiedSubject .isContext (tenant (), subject )) {
1270+ log .info ("Recursively deleting mode for all subjects under context: {}" , subject );
1271+ try {
1272+ deleteModesForSubjectsUnderContext (subject );
1273+ } catch (SchemaRegistryException e ) {
1274+ throw new SchemaRegistryStoreException (
1275+ "Failed to recursively delete modes for subjects under context" , e );
1276+ }
1277+ }
12571278 } catch (StoreException e ) {
12581279 throw new SchemaRegistryStoreException ("Failed to delete subject config value from store" ,
12591280 e );
12601281 }
12611282 }
12621283
1263- public void deleteSubjectModeOrForward (String subject , Map <String , String > headerProperties )
1284+ public void deleteSubjectModeOrForward (String subject , boolean recursive ,
1285+ Map <String , String > headerProperties )
12641286 throws SchemaRegistryStoreException , SchemaRegistryRequestForwardingException ,
12651287 OperationNotPermittedException , UnknownLeaderException {
12661288 kafkaStore .lockFor (subject ).lock ();
12671289 try {
12681290 if (isLeader ()) {
1269- deleteSubjectMode (subject );
1291+ // Delete the subject/context mode itself (and recursively if requested)
1292+ deleteSubjectMode (subject , recursive );
12701293 } else {
1271- // forward delete subject config request to the leader
1294+ // forward delete subject mode request to the leader (with recursive flag)
12721295 if (leaderIdentity != null ) {
1273- forwardDeleteSubjectModeRequestToLeader (subject , headerProperties );
1296+ forwardDeleteSubjectModeRequestToLeader (subject , recursive , headerProperties );
12741297 } else {
1275- throw new UnknownLeaderException ("Delete config request failed since leader is "
1298+ throw new UnknownLeaderException ("Delete mode request failed since leader is "
12761299 + "unknown" );
12771300 }
12781301 }
@@ -1281,6 +1304,68 @@ public void deleteSubjectModeOrForward(String subject, Map<String, String> heade
12811304 }
12821305 }
12831306
1307+ /**
1308+ * List all subjects that have a mode configured under the given prefix.
1309+ * This is different from listSubjectsWithPrefix which only returns subjects with schemas.
1310+ *
1311+ * @param prefix The subject prefix to match (e.g., ":.context:" for a context)
1312+ * @return Set of subject names that have modes configured under the prefix
1313+ * @throws SchemaRegistryException if there's an error accessing the store
1314+ */
1315+ private Set <String > listSubjectsWithModePrefix (String prefix )
1316+ throws SchemaRegistryException {
1317+ try {
1318+ ModeKey startKey = new ModeKey (prefix + Character .MIN_VALUE );
1319+ ModeKey endKey = new ModeKey (prefix + Character .MAX_VALUE );
1320+
1321+ Set <String > subjects = new LinkedHashSet <>();
1322+ try (CloseableIterator <SchemaRegistryValue > iterator =
1323+ kafkaStore .getAll (startKey , endKey )) {
1324+ while (iterator .hasNext ()) {
1325+ SchemaRegistryValue value = iterator .next ();
1326+ if (value instanceof ModeValue ) {
1327+ ModeValue modeValue = (ModeValue ) value ;
1328+ String subject = modeValue .getSubject ();
1329+ // Only include subjects that match our prefix
1330+ if (subject != null && subject .startsWith (prefix )) {
1331+ subjects .add (subject );
1332+ }
1333+ }
1334+ }
1335+ }
1336+ return subjects ;
1337+ } catch (StoreException e ) {
1338+ throw new SchemaRegistryStoreException (
1339+ "Error while retrieving subjects with modes under prefix: " + prefix , e );
1340+ }
1341+ }
1342+
1343+ private void deleteModesForSubjectsUnderContext (String context )
1344+ throws SchemaRegistryException {
1345+ // Context is already normalized and includes the trailing delimiter
1346+ // For default context: context would be like ":tenant:"
1347+ // For named context: context would be like ":.production:"
1348+ String subjectPrefix = context != null ? context :
1349+ QualifiedSubject .normalize (tenant (), CONTEXT_PREFIX + CONTEXT_DELIMITER );
1350+
1351+ // Get all subjects with modes under this context
1352+ // This includes subjects that have modes set but no schemas registered
1353+ Set <String > subjects = listSubjectsWithModePrefix (subjectPrefix );
1354+
1355+ log .info ("Found {} subjects with modes under context '{}' for recursive mode deletion with"
1356+ + " subjectPrefix={}" , subjects .size (), context , subjectPrefix );
1357+
1358+ // Delete mode for each subject (locally on leader)
1359+ int successCount = 0 ;
1360+ for (String subjectName : subjects ) {
1361+ log .debug ("Deleting mode for subject: {}" , subjectName );
1362+ deleteSubjectMode (subjectName );
1363+ successCount ++;
1364+ }
1365+
1366+ log .info ("Recursive mode deletion completed successfully for {} subjects" , successCount );
1367+ }
1368+
12841369 @ Override
12851370 public KafkaStore <SchemaRegistryKey , SchemaRegistryValue > getKafkaStore () {
12861371 return this .kafkaStore ;
0 commit comments