Skip to content

Flink: Close catalog opened by the dynamic sink serializer cache - #17437

Open
vishnuprakaz wants to merge 7 commits into
apache:mainfrom
vishnuprakaz:flink-dynamic-close-catalogs
Open

Flink: Close catalog opened by the dynamic sink serializer cache#17437
vishnuprakaz wants to merge 7 commits into
apache:mainfrom
vishnuprakaz:flink-dynamic-close-catalogs

Conversation

@vishnuprakaz

Copy link
Copy Markdown
Contributor

The dynamic sink's TableSerializerCache loads a catalog with CatalogLoader.loadCatalog() on every cache miss (an unknown schema or spec id, or an LRU eviction) but never closes it. Each call builds a fresh catalog whose resources, such as a REST HTTP client and thread pool or a Hive metastore client pool, are released only on close, so a long-running job that keeps missing the cache leaks connections and threads.

The cache lives inside a Flink TypeSerializer that has no teardown hook, so it now closes the catalog right after reading the table's schemas and specs. This matches how TableLoader closes the catalogs it opens.

@github-actions github-actions Bot added the flink label Jul 30, 2026
@pvary
pvary requested a review from mxm August 3, 2026 14:18

@mxm mxm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @vishnuprakaz!

Comment on lines +139 to +143
Catalog catalog = mock(Catalog.class, withSettings().extraInterfaces(Closeable.class));
when(catalog.loadTable(any(TableIdentifier.class))).thenReturn(table);
CatalogLoader catalogLoader = mock(CatalogLoader.class);
when(catalogLoader.loadCatalog()).thenReturn(catalog);
cache = new TableSerializerCache(catalogLoader, 10);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we write this test without using Mockito?

}
}

private static class SingleCatalogLoader implements CatalogLoader {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace this with HadoopCatalogExtension?

Comment on lines +138 to +144
if (catalog instanceof Closeable) {
try {
((Closeable) catalog).close();
} catch (IOException e) {
LOG.warn("Failed to close catalog {}", catalog.name(), e);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be feasible to let the CatalogLoader manage the lifecycle of the catalog and reuse it when necessary? It is a bit odd to check for Closable when the Catalog interface doesn't contain this interface.

@vishnuprakaz vishnuprakaz Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On this I looked into having the loader manage and reuse the catalog. the problem I ran into is that the serializer has no teardown hook, so as far as I can tell nothing at this layer could ever close a held catalog. Does that match your thinking that the reuse part belongs in the TaskManager level cache?

While checking, I noticed loadCatalog() has six other call sites in the sink that don't cloae the catalog either. The operator-level ones do have close() hooks, would it be worth a similar interim fix for those, or better to leave them for the TaskManager level cache work?

For the Closeable check here.... would it make sense to switch update() to TableLoader.fromCatalog() in a try with resources, like FlinkSink and IcebergSink do for one shot loads?
but the lifecycle (and the Closeable check) would live inside TableLoader instead of this class.

@vishnuprakaz
vishnuprakaz requested a review from mxm August 5, 2026 17:45
Comment on lines +131 to +140
try (TableLoader tableLoader =
TableLoader.fromCatalog(catalogLoader, TableIdentifier.parse(tableName))) {
tableLoader.open();
Table table = tableLoader.loadTable();
schemas = table.schemas();
specs = table.specs();
} catch (IOException e) {
// only close() throws IOException here; a failed close should not fail the lookup
LOG.warn("Failed to close catalog for table {}", tableName, e);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure loading / closing on every update is a good idea. Can we store Catalog in a field? This will get rid of the duplicate loading and only load once per job.

If we are concerned about not closing one instance, we could close it via RuntimeContext#registerUserCodeClassLoaderReleaseHookIfAbsent, but I'm not sure this is necessary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes a lot of sense, loading once and reusing is better trade actually. I was thinking per table and missed how many tables the dynamic sink could be routing. Made the changes, happy to adjust if anything looks off :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants