Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3e72ffe
fix(glue-alpha): fix partition index race condition when creating mul…
invalid-email-address May 27, 2026
c046164
remove ray snapshots
Abogical Jun 3, 2026
f9ad55b
restore accidentally deleted base-path-mapping snapshot
Abogical Jun 3, 2026
0fa1b0d
fix: wrong assertions used in integ test.
Abogical Jun 4, 2026
797aa8c
merge from main
otaviomacedo Jul 13, 2026
1a41999
check existing partition indexes
otaviomacedo Jul 13, 2026
87b8de4
bring back files and just delete when the index has disappeared
otaviomacedo Jul 13, 2026
8b0be22
Add unit tests for the handler
otaviomacedo Jul 13, 2026
e194313
bundle dependency
otaviomacedo Jul 13, 2026
55974fd
deduplicate grants
otaviomacedo Jul 13, 2026
bfc5945
Add bundling script for partition index handler and update dependencies
otaviomacedo Jul 14, 2026
241b199
Updated snapshots
otaviomacedo Jul 14, 2026
fb88275
token validation, unique id
otaviomacedo Jul 15, 2026
40c78c7
bundled js
otaviomacedo Jul 15, 2026
3d96f57
reduce timeout
otaviomacedo Jul 15, 2026
9f0a125
Merge branch 'main' into glue-fix-partition-index
otaviomacedo Jul 15, 2026
e10de05
update snapshots
otaviomacedo Jul 15, 2026
c250d25
update snapshots
otaviomacedo Jul 15, 2026
62bfd24
new pkglint rule, snapshot update
otaviomacedo Jul 15, 2026
944a70c
timeout
otaviomacedo Jul 15, 2026
132c295
handling edge cases around create
otaviomacedo Jul 15, 2026
e785556
unique ids in case of tokens
otaviomacedo Jul 15, 2026
6e0f924
key order-sensitive comparison
otaviomacedo Jul 15, 2026
7217077
improvements to the state machine
otaviomacedo Jul 17, 2026
d84717b
Simplifications
otaviomacedo Jul 17, 2026
da247a7
handle ResourceNumberLimitExceededException
otaviomacedo Jul 28, 2026
0c74d8d
handle ResourceNumberLimitExceededException
otaviomacedo Jul 28, 2026
3cdffed
update test
otaviomacedo Jul 28, 2026
93f990f
Merge branch 'main' into glue-fix-partition-index
mergify[bot] Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,20 +131,20 @@
"@aws-cdk/core/table/**",
"@aws-cdk/core/yaml",
"@aws-cdk/core/yaml/**",
"@aws-cdk/cx-api/semver",
"@aws-cdk/cx-api/semver/**",
"@aws-cdk/cx-api/@aws-cdk/cloud-assembly-api",
"@aws-cdk/cx-api/@aws-cdk/cloud-assembly-api/**",
"@aws-cdk/cx-api/semver",
"@aws-cdk/cx-api/semver/**",
"@aws-cdk/mixins-preview/minimatch",
"@aws-cdk/mixins-preview/minimatch/**",
"@aws-cdk/pipelines/aws-sdk",
"@aws-cdk/pipelines/aws-sdk/**",
"@aws-cdk/yaml-cfn/yaml",
"@aws-cdk/yaml-cfn/yaml/**",
"aws-cdk-lib/@balena/dockerignore",
"aws-cdk-lib/@balena/dockerignore/**",
"aws-cdk-lib/@aws-cdk/cloud-assembly-api",
"aws-cdk-lib/@aws-cdk/cloud-assembly-api/**",
"aws-cdk-lib/@balena/dockerignore",
"aws-cdk-lib/@balena/dockerignore/**",
"aws-cdk-lib/case",
"aws-cdk-lib/case/**",
"aws-cdk-lib/chalk",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { GlueClient, CreatePartitionIndexCommand, DeletePartitionIndexCommand, GetPartitionIndexesCommand } from '@aws-sdk/client-glue';

class PartitionIndexError extends Error {
constructor(message: string) {
super(message);
this.name = 'PartitionIndexError';
}
}

const glue = new GlueClient({});

export async function onEvent(event: any) {
const { DatabaseName, TableName, IndexName, Keys } = event.ResourceProperties;

if (event.RequestType === 'Create') {
await glue.send(new CreatePartitionIndexCommand({
Comment thread
otaviomacedo marked this conversation as resolved.
Outdated
DatabaseName,
TableName,
PartitionIndex: { IndexName, Keys },
}));
return { PhysicalResourceId: IndexName };
} else if (event.RequestType === 'Update') {
const oldKeys = event.OldResourceProperties?.Keys;
if (JSON.stringify(oldKeys) !== JSON.stringify(Keys)) {
// For auto-generated index names, this should be unreachable. CDK derives index names from keys, so key changes always produce a new resource
throw new PartitionIndexError('Partition index keys cannot be updated. Delete and recreate the index instead.');
}
return { PhysicalResourceId: IndexName };
} else if (event.RequestType === 'Delete') {
try {
await glue.send(new DeletePartitionIndexCommand({
DatabaseName,
TableName,
IndexName,
}));
} catch (e: any) {
if (e.name === 'EntityNotFoundException') {
// eslint-disable-next-line no-console
console.log(`Partition index ${IndexName} not found on ${DatabaseName}.${TableName} - may have been deleted out-of-band`);
} else {
throw e;
}
}
}

return {};
}

export async function isComplete(event: any) {
const { DatabaseName, TableName, IndexName } = event.ResourceProperties;

if (event.RequestType === 'Delete') {
const resp = await glue.send(new GetPartitionIndexesCommand({ DatabaseName, TableName }));
Comment thread
otaviomacedo marked this conversation as resolved.
Outdated
const index = (resp.PartitionIndexDescriptorList || []).find(
(i) => i.IndexName!.toLowerCase() === IndexName.toLowerCase(),
);
if (!index || index.IndexStatus !== 'DELETING') {
return { IsComplete: true };
}
return { IsComplete: false };
}

const resp = await glue.send(new GetPartitionIndexesCommand({ DatabaseName, TableName }));
// Glue lowercases index names, so compare case-insensitively
const index = (resp.PartitionIndexDescriptorList || []).find(
(i) => i.IndexName!.toLowerCase() === IndexName.toLowerCase(),
);

if (!index) return { IsComplete: false };
if (index.IndexStatus === 'ACTIVE') return { IsComplete: true };
if (index.IndexStatus === 'CREATING') return { IsComplete: false };

const errorDetails = index.BackfillErrors?.length
? ` Backfill errors: ${JSON.stringify(index.BackfillErrors)}`
: '';
throw new PartitionIndexError(`Partition index ${IndexName} in unexpected state: ${index.IndexStatus}.${errorDetails}`);
}
97 changes: 70 additions & 27 deletions packages/@aws-cdk/aws-glue-alpha/lib/table-base.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as path from 'path';
import type { CfnTable } from 'aws-cdk-lib/aws-glue';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import type { IResource } from 'aws-cdk-lib/core';
import { ArnFormat, Fn, Lazy, Names, Resource, Stack, UnscopedValidationError, ValidationError } from 'aws-cdk-lib/core';
import { ArnFormat, CustomResource, Duration, Fn, Lazy, Names, Resource, Stack, Token, UnscopedValidationError, ValidationError } from 'aws-cdk-lib/core';
import { lit } from 'aws-cdk-lib/core/lib/helpers-internal';
import * as cr from 'aws-cdk-lib/custom-resources';
import type { AwsCustomResource } from 'aws-cdk-lib/custom-resources';
import type { Construct } from 'constructs';
import type { DataFormat } from './data-format';
import type { IDatabase } from './database';
Expand Down Expand Up @@ -254,7 +255,7 @@ export abstract class TableBase extends Resource implements ITable {
* race conditions, we store the resource and add dependencies
* each time a new partition index is created.
*/
private partitionIndexCustomResources: AwsCustomResource[] = [];
private partitionIndexCustomResources: CustomResource[] = [];

constructor(scope: Construct, id: string, props: TableBaseProps) {
super(scope, id, {
Expand Down Expand Up @@ -298,37 +299,79 @@ export abstract class TableBase extends Resource implements ITable {
this.validatePartitionIndex(index);

const indexName = index.indexName ?? this.generateIndexName(index.keyNames);
const partitionIndexCustomResource = new cr.AwsCustomResource(this, `partition-index-${indexName}`, {
onCreate: {
service: 'Glue',
action: 'createPartitionIndex',
parameters: {
DatabaseName: this.database.databaseName,
TableName: this.tableName,
PartitionIndex: {
IndexName: indexName,
Keys: index.keyNames,
},
},
physicalResourceId: cr.PhysicalResourceId.of(
indexName,
),
const { provider, handler, isCompleteHandler } = this.getOrCreatePartitionIndexProvider();

// Add scoped permissions for this table's Glue resources
// https://docs.aws.amazon.com/service-authorization/latest/reference/list_awsglue.html
const tablePolicy = new iam.PolicyStatement({
actions: ['glue:CreatePartitionIndex', 'glue:DeletePartitionIndex', 'glue:GetPartitionIndexes', 'glue:GetTable', 'glue:UpdateTable'],
resources: [this.tableArn, this.database.databaseArn, this.database.catalogArn],
});
handler.addToRolePolicy(tablePolicy);
isCompleteHandler.addToRolePolicy(tablePolicy);
Comment thread
otaviomacedo marked this conversation as resolved.
Outdated

const partitionIndexCustomResource = new CustomResource(this, `partition-index-${indexName}`, {
resourceType: 'Custom::GluePartitionIndex',
serviceToken: provider.serviceToken,
properties: {
DatabaseName: this.database.databaseName,
TableName: this.tableName,
IndexName: indexName,
Keys: index.keyNames,
},
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
// APIs are available in 2.1055.0
installLatestAwsSdk: false,
});
this.grantToUnderlyingResources(partitionIndexCustomResource, ['glue:UpdateTable']);

// Depend on previous partition index if possible, to avoid race condition
// Ensure IAM policies are created before the custom resource invokes the handlers
partitionIndexCustomResource.node.addDependency(handler.role!);
partitionIndexCustomResource.node.addDependency(isCompleteHandler.role!);

// Depend on previous partition index to avoid race condition
if (numPartitions > 0) {
this.partitionIndexCustomResources[numPartitions-1].node.addDependency(partitionIndexCustomResource);
partitionIndexCustomResource.node.addDependency(this.partitionIndexCustomResources[numPartitions-1]);
}
this.partitionIndexCustomResources.push(partitionIndexCustomResource);
}

private getOrCreatePartitionIndexProvider(): { provider: cr.Provider; handler: lambda.Function; isCompleteHandler: lambda.Function } {
const providerId = 'GluePartitionIndexProvider';
const stack = Stack.of(this);
const existingProvider = stack.node.tryFindChild(providerId) as cr.Provider;
if (existingProvider) {
return {
provider: existingProvider,
handler: stack.node.findChild('GluePartitionIndexHandler') as lambda.Function,
isCompleteHandler: stack.node.findChild('GluePartitionIndexIsComplete') as lambda.Function,
};
}

const handler = new lambda.Function(stack, 'GluePartitionIndexHandler', {
runtime: lambda.determineLatestNodeRuntime(stack),
code: lambda.Code.fromAsset(path.join(__dirname, 'partition-index-handler'), {
exclude: ['*.ts', '*.d.ts'],
}),
handler: 'index.onEvent',
timeout: Duration.minutes(1),
});

const isCompleteHandler = new lambda.Function(stack, 'GluePartitionIndexIsComplete', {
runtime: lambda.determineLatestNodeRuntime(stack),
code: lambda.Code.fromAsset(path.join(__dirname, 'partition-index-handler'), {
exclude: ['*.ts', '*.d.ts'],
}),
handler: 'index.isComplete',
timeout: Duration.minutes(1),
});

const provider = new cr.Provider(stack, providerId, {
onEventHandler: handler,
isCompleteHandler: isCompleteHandler,
queryInterval: Duration.seconds(10),
totalTimeout: Duration.hours(1),
});

return { provider, handler, isCompleteHandler };
}

private generateIndexName(keys: string[]): string {
const prefix = keys.join('-') + '-';
const uniqueId = Names.uniqueId(this);
Expand All @@ -338,7 +381,7 @@ export abstract class TableBase extends Resource implements ITable {
}

private validatePartitionIndex(index: PartitionIndex) {
if (index.indexName !== undefined && (index.indexName.length < 1 || index.indexName.length > 255)) {
if (index.indexName !== undefined && !Token.isUnresolved(index.indexName) && (index.indexName.length < 1 || index.indexName.length > 255)) {
throw new ValidationError(lit`IndexNameLengthInvalid`, `Index name must be between 1 and 255 characters, but got ${index.indexName.length}`, this);
}
if (!this.partitionKeys || this.partitionKeys.length === 0) {
Expand Down
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-glue-alpha/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"@aws-cdk/integ-runner": "^2.198.0",
"@aws-cdk/integ-tests-alpha": "0.0.0",
"@aws-cdk/pkglint": "0.0.0",
"@aws-sdk/client-glue": "3.632.0",
"@types/jest": "^29.5.14",
"aws-cdk-lib": "0.0.0",
"constructs": "^10.5.0",
Expand All @@ -112,5 +113,6 @@
"naming/package-matches-directory",
"assert/assert-dependency"
]
}
},
"dependencies": {}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading