Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
78 changes: 37 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ const client = new AlternatorDynamoDBClient({
seeds: ["scylla-0.internal", "scylla-1.internal"],
scheme: "http",
port: 8080,
routing: routing.datacenter("dc1", {
routing: routing.datacenter({
datacenter: "dc1",
fallback: routing.cluster(),
}),

Expand All @@ -51,7 +52,7 @@ await client.send(
client.destroy();
```

`endpoint` is rejected. Use `seeds`, optional `scheme`, and a shared `port`.
The public config does not include `endpoint`. Use `seeds`, optional `scheme`, and a shared `port`.
Defaults are `scheme: "http"` and `port: 8080`. HTTPS users usually set
`scheme: "https"` and `port: 8043`.

Expand All @@ -65,7 +66,7 @@ signing is preserved. `region` defaults to `us-east-1` for signing.
import { PutCommand } from "@aws-sdk/lib-dynamodb";
import { AlternatorDynamoDBDocumentClient } from "@scylladb/alternator-client/document";

const docClient = new AlternatorDynamoDBDocumentClient(
const docClient = AlternatorDynamoDBDocumentClient.fromConfig(
{ seeds: ["localhost"] },
{ marshallOptions: { removeUndefinedValues: true } },
);
Expand All @@ -78,7 +79,7 @@ await docClient.send(
);
```

AWS-style wrapping is also supported:
AWS-style wrapping is the primary API when you already have a low-level client:

```ts
const base = new AlternatorDynamoDBClient({ seeds: ["localhost"] });
Expand All @@ -91,21 +92,25 @@ balancing unless the passed client is already an `AlternatorDynamoDBClient`.
## Alternator APIs

```ts
client.getLiveNodes();
await client.refreshLiveNodes();
await client.checkRackDatacenterSupport();
await client.checkIfRackAndDatacenterSetCorrectly();
await client.validateRackDatacenterConfig();
client.getPartitionKeyName("users");
client.alternator.nodes();
await client.alternator.refreshNodes();
await client.alternator.supportsScopedDiscovery();
await client.alternator.validateRouting();
client.alternator.partitionKey("users");
```

Routing helpers:

```ts
routing.cluster();
routing.datacenter("dc1", { fallback: routing.cluster() });
routing.rack("dc1", "rack1", {
fallback: routing.datacenter("dc1", { fallback: routing.cluster() }),
routing.datacenter({ datacenter: "dc1", fallback: routing.cluster() });
routing.rack({
datacenter: "dc1",
rack: "rack1",
fallback: routing.datacenter({
datacenter: "dc1",
fallback: routing.cluster(),
}),
});
```

Expand Down Expand Up @@ -152,43 +157,42 @@ new AlternatorDynamoDBClient({

compression: {
request: {
enabled: true,
thresholdBytes: 1_024,
gzipLevel: -1,
},
response: {
enabled: true,
algorithms: [ResponseCompressionGzip],
algorithms: ["gzip"],
},
},

headerOptimization: {
enabled: true,
allowedHeaders: ["Host", "X-Amz-Target", "Content-Length", "Accept-Encoding", "Content-Encoding"],
},

userAgent: (userAgent) => `${userAgent} my-app/1.2.3`,
userAgent: { append: "my-app/1.2.3" },

keyRouteAffinity: {
type: "any-write",
mode: "any-write",
partitionKeys: {
users: "id",
},
autoDiscoverPartitionKeys: true,
},

tls: {
caFile: "/etc/ssl/scylla-ca.pem",
ca: { file: "/etc/ssl/scylla-ca.pem" },
rejectUnauthorized: true,
sessionCache: true,
},

connection: {
keepAlive: true,
maxSockets: 50,
connectionTimeoutMs: 1_000,
requestTimeoutMs: 0,
socketTimeoutMs: 0,
timeouts: {
connectMs: 1_000,
requestMs: 0,
socketMs: 0,
},
},
});
```
Expand Down Expand Up @@ -217,17 +221,16 @@ You can replace it completely:
```ts
new AlternatorDynamoDBClient({
seeds: ["scylla-0.internal"],
userAgent: "my-client/1.2.3",
userAgent: { value: "my-client/1.2.3" },
});
```

You can transform the generated value. The function receives the default
ScyllaDB Alternator user-agent:
You can append to the generated value:

```ts
new AlternatorDynamoDBClient({
seeds: ["scylla-0.internal"],
userAgent: (userAgent) => `${userAgent} my-app/4.5.6`,
userAgent: { append: "my-app/4.5.6" },
});
```

Expand All @@ -239,37 +242,30 @@ separately:
```ts
compression: {
request: {
enabled: true,
thresholdBytes: 1_024,
gzipLevel: -1,
},
response: {
enabled: true,
algorithms: [ResponseCompressionGzip],
algorithms: ["gzip"],
},
}
```

`compression.request: true` compresses every measurable request body with gzip.
`compression.request: {}` compresses every measurable request body with gzip.
Use `thresholdBytes` to skip smaller request bodies, `gzipLevel` to select the
zlib level, or `compressor` for a custom request compressor.
zlib level, or `compressor` for a custom request compressor. Use
`compression.request: false` to disable request compression explicitly.

`compression.response: true` enables the default response algorithm, `gzip`.
`compression.response: {}` enables the default response algorithm, `gzip`.
Pass an options object with an explicit algorithm list to control the
`Accept-Encoding` value:

```ts
import {
ResponseCompressionDeflate,
ResponseCompressionGzip,
} from "@scylladb/alternator-client";

new AlternatorDynamoDBClient({
seeds: ["scylla-0.internal"],
compression: {
response: {
enabled: true,
algorithms: [ResponseCompressionGzip, ResponseCompressionDeflate],
algorithms: ["gzip", "deflate"],
},
},
});
Expand All @@ -284,7 +280,7 @@ Key-route affinity supports these modes:

```ts
keyRouteAffinity: {
type: "read-before-write", // or "any-write"
mode: "read-before-write", // or "any-write"
partitionKeys: { users: "id" },
}
```
Expand Down
39 changes: 15 additions & 24 deletions src/affinity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AlternatorQueryPlan, firstNodeWithSeed } from "./query-plan.js";
import { murmur3H1 } from "./murmur.js";
import type {
AlternatorKeyRouteAffinityType,
AlternatorKeyRouteAffinityMode,
AlternatorLogger,
AlternatorNode,
NormalizedKeyRouteAffinityOptions,
Expand Down Expand Up @@ -48,11 +48,11 @@ export class KeyRouteAffinityPlanner {
return undefined;
}

if (isBatchWriteCommand(commandName) && isBatchWriteInput(input) && this.config.type === "any-write") {
if (isBatchWriteCommand(commandName) && isBatchWriteInput(input) && this.config.mode === "any-write") {
return this.batchWriteQueryPlan(input.RequestItems, nodes);
}

const hash = this.partitionKeyHashForInput(input, this.config.type, commandName);
const hash = this.partitionKeyHashForInput(input, this.config.mode, commandName);
if (hash === undefined) {
return undefined;
}
Expand All @@ -61,10 +61,10 @@ export class KeyRouteAffinityPlanner {

private partitionKeyHashForInput(
input: DynamoDBInput,
affinityType: AlternatorKeyRouteAffinityType,
affinityMode: AlternatorKeyRouteAffinityMode,
commandName?: string,
): bigint | undefined {
const request = writeRequestForInput(input, affinityType, commandName);
const request = writeRequestForInput(input, affinityMode, commandName);
if (!request) {
return undefined;
}
Expand Down Expand Up @@ -171,48 +171,42 @@ function hashWithPrefix(prefix: number, bytes: Uint8Array): bigint {

function writeRequestForInput(
input: DynamoDBInput,
affinityType: AlternatorKeyRouteAffinityType,
affinityMode: AlternatorKeyRouteAffinityMode,
commandName?: string,
): { tableName: string; values: AttributeValueRecord } | undefined {
const tableName = typeof input.TableName === "string" ? input.TableName : undefined;
if (!tableName) {
return undefined;
}

if (isPutCommand(commandName) && isRecord(input.Item) && shouldRoutePut(input, affinityType)) {
if (isPutCommand(commandName) && isRecord(input.Item) && shouldRoutePut(input, affinityMode)) {
return { tableName, values: input.Item };
}
if (isUpdateCommand(commandName) && isRecord(input.Key) && shouldRouteUpdate(input, affinityType)) {
if (isUpdateCommand(commandName) && isRecord(input.Key) && shouldRouteUpdate(input, affinityMode)) {
return { tableName, values: input.Key };
}
if (isDeleteCommand(commandName) && isRecord(input.Key) && shouldRouteDelete(input, affinityType)) {
if (isDeleteCommand(commandName) && isRecord(input.Key) && shouldRouteDelete(input, affinityMode)) {
return { tableName, values: input.Key };
}

return undefined;
}

function shouldRoutePut(input: DynamoDBInput, affinityType: AlternatorKeyRouteAffinityType): boolean {
function shouldRoutePut(input: DynamoDBInput, affinityMode: AlternatorKeyRouteAffinityMode): boolean {
if (!("Item" in input)) {
return false;
}
if (affinityType === "none") {
return false;
}
if (affinityType === "any-write") {
if (affinityMode === "any-write") {
return true;
}
return hasExpected(input) || nonEmptyString(input.ConditionExpression) || input.ReturnValues === "ALL_OLD";
}

function shouldRouteUpdate(input: DynamoDBInput, affinityType: AlternatorKeyRouteAffinityType): boolean {
function shouldRouteUpdate(input: DynamoDBInput, affinityMode: AlternatorKeyRouteAffinityMode): boolean {
if (!("Key" in input) || !("UpdateExpression" in input || "AttributeUpdates" in input)) {
return false;
}
if (affinityType === "none") {
return false;
}
if (affinityType === "any-write") {
if (affinityMode === "any-write") {
return true;
}

Expand Down Expand Up @@ -241,14 +235,11 @@ function shouldRouteUpdate(input: DynamoDBInput, affinityType: AlternatorKeyRout
return false;
}

function shouldRouteDelete(input: DynamoDBInput, affinityType: AlternatorKeyRouteAffinityType): boolean {
function shouldRouteDelete(input: DynamoDBInput, affinityMode: AlternatorKeyRouteAffinityMode): boolean {
if (!("Key" in input) || "UpdateExpression" in input || "AttributeUpdates" in input) {
return false;
}
if (affinityType === "none") {
return false;
}
if (affinityType === "any-write") {
if (affinityMode === "any-write") {
return true;
}
return hasExpected(input) || nonEmptyString(input.ConditionExpression) || input.ReturnValues === "ALL_OLD";
Expand Down
49 changes: 21 additions & 28 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@ import { createAlternatorPostSigningMiddleware, createAlternatorRequestMiddlewar
import { assertRuntimeSupport, createRequestHandler } from "./runtime.js";
import type { AlternatorDynamoDBClientConfig, AlternatorNode, NormalizedAlternatorConfig } from "./types.js";

export interface AlternatorDynamoDBClientApi {
nodes(): AlternatorNode[];
refreshNodes(): Promise<AlternatorNode[]>;
supportsScopedDiscovery(): Promise<boolean>;
validateRouting(): Promise<void>;
partitionKey(tableName: string): string | undefined;
}

export class AlternatorDynamoDBClient extends DynamoDBClient {
readonly alternatorConfig: NormalizedAlternatorConfig;
readonly alternator: AlternatorDynamoDBClientApi;
private readonly alternatorConfig: NormalizedAlternatorConfig;
private readonly discovery: AlternatorDiscovery;
private readonly keyAffinity: KeyRouteAffinityPlanner;

Expand All @@ -38,6 +47,13 @@ export class AlternatorDynamoDBClient extends DynamoDBClient {
(tableName) => this.discoverPartitionKey(tableName),
alternatorConfig.logger,
);
this.alternator = {
nodes: () => this.discovery.getLiveNodes(),
refreshNodes: () => this.discovery.refreshLiveNodes(),
supportsScopedDiscovery: () => this.discovery.checkRackDatacenterSupport(),
validateRouting: () => this.discovery.checkIfRackAndDatacenterSetCorrectly(),
partitionKey: (tableName) => this.keyAffinity.getPartitionKeyName(tableName),
};

this.middlewareStack.addRelativeTo(
createAlternatorRequestMiddleware<ServiceInputTypes, ServiceOutputTypes>({
Expand Down Expand Up @@ -65,30 +81,6 @@ export class AlternatorDynamoDBClient extends DynamoDBClient {

}

getLiveNodes(): AlternatorNode[] {
return this.discovery.getLiveNodes();
}

refreshLiveNodes(): Promise<AlternatorNode[]> {
return this.discovery.refreshLiveNodes();
}

checkRackDatacenterSupport(): Promise<boolean> {
return this.discovery.checkRackDatacenterSupport();
}

checkIfRackAndDatacenterSetCorrectly(): Promise<void> {
return this.discovery.checkIfRackAndDatacenterSetCorrectly();
}

validateRackDatacenterConfig(): Promise<void> {
return this.checkIfRackAndDatacenterSetCorrectly();
}

getPartitionKeyName(tableName: string): string | undefined {
return this.keyAffinity.getPartitionKeyName(tableName);
}

override destroy(): void {
this.discovery.destroy();
super.destroy();
Expand Down Expand Up @@ -125,11 +117,12 @@ function buildDynamoConfig(
tls: _tls,
discovery: _discovery,
connection: _connection,
endpoint: _endpoint,
logger: _logger,
credentials,
region,
...awsConfig
} = input;
...awsConfigWithEndpoint
} = input as AlternatorDynamoDBClientConfig & { endpoint?: unknown };
const { endpoint: _endpoint, ...awsConfig } = awsConfigWithEndpoint;

const dynamoConfig: DynamoDBClientConfig & { applyChecksum?: boolean } = {
...awsConfig,
Expand Down
Loading