Skip to content

Commit

Permalink
feat: implement ProvisionStorage in terms of subscription and consu…
Browse files Browse the repository at this point in the history
…mer tables (#200)

Implement the subscription and consumer tables specified by @Gozala in
storacha/w3up#746 and use them to implement
a new version of ProvisionStorage

This builds on a PR review from @Gozala that lays out some of the
queries we will need to support with these new tables:


#194 (review)

This is configured to merge into the main "D1 to Dynamo" development
branch - I'm in favor of going to production with this implementation of
`ProvisionStorage` rather than the one in that branch now.
  • Loading branch information
Travis Vachon authored May 23, 2023
1 parent fcebd25 commit aafd061
Show file tree
Hide file tree
Showing 18 changed files with 1,273 additions and 512 deletions.
956 changes: 534 additions & 422 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 15 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
"@serverless-stack/resources": "^1.18.0",
"@tsconfig/node16": "^1.0.3",
"@types/git-rev-sync": "^2.0.0",
"@ucanto/client": "^7.0.1",
"@ucanto/core": "^7.1.1",
"@ucanto/interface": "^7.1.0",
"@ucanto/principal": "^7.0.0",
"@ucanto/transport": "^7.0.3",
"@ucanto/client": "^8.0.0",
"@ucanto/core": "^8.0.0",
"@ucanto/interface": "^8.0.0",
"@ucanto/principal": "^8.0.0",
"@ucanto/transport": "^8.0.0",
"@ucanto/validator": "^8.0.0",
"@web-std/blob": "^3.0.4",
"@web-std/fetch": "^4.1.0",
"@web3-storage/access": "^13.0.0",
"@web3-storage/w3up-client": "^6.0.0",
"ava": "^4.3.3",
"dotenv": "^16.0.3",
Expand All @@ -53,10 +53,13 @@
},
"eslintConfig": {
"extends": [
"./node_modules/hd-scripts/eslint/index.js"
"./node_modules/hd-scripts/eslint/preact.js"
],
"parserOptions": {
"project": "./tsconfig.json"
"project": "./tsconfig.json",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"unicorn/prefer-number-properties": "off",
Expand All @@ -72,14 +75,17 @@
"unicorn/explicit-length-check": "off",
"unicorn/prefer-type-error": "off",
"unicorn/no-zero-fractions": "off",
"unicorn/expiring-todo-comments": "off",
"eqeqeq": "off",
"no-new": "off",
"no-void": "off",
"no-console": "off",
"no-continue": "off",
"no-warning-comments": "off",
"jsdoc/check-indentation": "off",
"jsdoc/require-hyphen-before-param-description": "off"
"jsdoc/require-hyphen-before-param-description": "off",
"react-hooks/rules-of-hooks": "off",
"react/no-danger": "off"
}
},
"workspaces": [
Expand Down
13 changes: 11 additions & 2 deletions stacks/upload-api-stack.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
Api,
Config,
Bucket,
use
} from '@serverless-stack/resources'
import { UploadDbStack } from './upload-db-stack.js'
import { CarparkStack } from './carpark-stack.js'
import { UcanInvocationStack } from './ucan-invocation-stack.js'

import { getCustomDomain, getApiPackageJson, getGitInfo, setupSentry } from './config.js'
import { getCustomDomain, getApiPackageJson, getGitInfo, setupSentry, getBucketConfig } from './config.js'

/**
* @param {import('@serverless-stack/resources').StackContext} properties
Expand All @@ -22,9 +23,17 @@ export function UploadApiStack({ stack, app }) {

// Get references to constructs created in other stacks
const { carparkBucket } = use(CarparkStack)
const { storeTable, uploadTable, provisionTable, delegationTable, delegationBucket, adminMetricsTable } = use(UploadDbStack)
const { storeTable, uploadTable, provisionTable, delegationTable, adminMetricsTable } = use(UploadDbStack)
const { invocationBucket, taskBucket, workflowBucket, ucanStream } = use(UcanInvocationStack)

// Resources for this stack
const delegationBucket = new Bucket(stack, 'delegation-store', {
cors: true,
cdk: {
bucket: getBucketConfig('delegation', app.stage)
}
})

// Setup API
const customDomain = getCustomDomain(stack.stage, process.env.HOSTED_ZONE)
const pkg = getApiPackageJson()
Expand Down
14 changes: 3 additions & 11 deletions stacks/upload-db-stack.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Bucket, Table } from '@serverless-stack/resources'
import { Table } from '@serverless-stack/resources'

import {
storeTableProps,
Expand All @@ -10,7 +10,7 @@ import {
adminMetricsTableProps,
spaceMetricsTableProps
} from '../ucan-invocation/tables/index.js'
import { getBucketConfig, setupSentry } from './config.js'
import { setupSentry } from './config.js'

/**
* @param {import('@serverless-stack/resources').StackContext} properties
Expand Down Expand Up @@ -38,17 +38,10 @@ export function UploadDbStack({ stack, app }) {
const provisionTable = new Table(stack, 'provision', provisionTableProps)

/**
* This table tracks metrics per space.
* This table indexes delegations.
*/
const delegationTable = new Table(stack, 'delegation', delegationTableProps)

const delegationBucket = new Bucket(stack, 'delegation-store', {
cors: true,
cdk: {
bucket: getBucketConfig('delegation', app.stage)
}
})

/**
* This table tracks w3 wider metrics.
*/
Expand All @@ -63,7 +56,6 @@ export function UploadDbStack({ stack, app }) {
storeTable,
uploadTable,
provisionTable,
delegationBucket,
delegationTable,
adminMetricsTable,
spaceMetricsTable
Expand Down
4 changes: 2 additions & 2 deletions ucan-invocation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
"@aws-sdk/client-dynamodb": "^3.226.0",
"@aws-sdk/client-eventbridge": "^3.218.0",
"@sentry/serverless": "^7.22.0",
"@web3-storage/capabilities": "^5.0.0",
"@web3-storage/capabilities": "^5.0.1",
"uint8arrays": "^4.0.2"
},
"devDependencies": {
"@aws-sdk/client-dynamodb": "^3.226.0",
"@aws-sdk/client-s3": "^3.226.0",
"@serverless-stack/resources": "*",
"@ucanto/interface": "^7.1.0",
"@ucanto/interface": "^8.0.0",
"ava": "^4.3.3",
"multiformats": "^11.0.1",
"nanoid": "4.0.0",
Expand Down
36 changes: 15 additions & 21 deletions upload-api/access.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as Space from '@web3-storage/capabilities/space'
import { Space } from '@web3-storage/capabilities'
import { connect } from '@ucanto/client'
import { Failure } from '@ucanto/server'
import { CAR, HTTP } from '@ucanto/transport'
Expand All @@ -24,27 +24,21 @@ export function createAccessClient(issuer, serviceDID, serviceURL) {
// if info capability is derivable from the passed capability, then we'll
// receive a response and know that the invocation issuer has verified
// themselves with w3access.
const info = Space.info.invoke({
issuer,
audience: serviceDID,
// @ts-expect-error
with: invocation.capabilities[0].with,
proofs: [invocation],
})
const { out: result } = await Space.info
.invoke({
issuer,
audience: serviceDID,
// @ts-expect-error
with: invocation.capabilities[0].with,
proofs: [invocation],
})
.execute(conn)

const { out: result } = await info.execute(conn)

if (result.error) {
return result.error.name === 'SpaceUnknown'
? {
error: new Failure(`Space has no storage provider`, {
cause: result,
}),
}
: result
} else {
return { ok: {} }
}
return result.error ? ({
error: new Failure(`Failed to get info about space, could not allocate.`, {
cause: result.error
})
}) : { ok: {} };
},
}
}
18 changes: 9 additions & 9 deletions upload-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
"@ipld/dag-ucan": "^3.0.1",
"@sentry/serverless": "^7.22.0",
"@serverless-stack/node": "^1.18.2",
"@ucanto/client": "^7.0.1",
"@ucanto/core": "^7.1.1",
"@ucanto/interface": "^7.1.0",
"@ucanto/principal": "^7.0.0",
"@ucanto/server": "^7.0.2",
"@ucanto/transport": "^7.0.3",
"@ucanto/client": "^8.0.0",
"@ucanto/core": "^8.0.0",
"@ucanto/interface": "^8.0.0",
"@ucanto/principal": "^8.0.0",
"@ucanto/server": "^8.0.1",
"@ucanto/transport": "^8.0.0",
"@ucanto/validator": "^8.0.0",
"@web-std/fetch": "^4.1.0",
"@web3-storage/access": "^13.0.0",
"@web3-storage/access": "^13.0.2",
"@web3-storage/capabilities": "file:../../w3up/packages/capabilities",
"@web3-storage/upload-api": "file:../../w3up/packages/upload-api",
"@web3-storage/w3infra-ucan-invocation": "*",
Expand All @@ -35,9 +36,8 @@
"@ipld/car": "^5.0.1",
"@serverless-stack/resources": "*",
"@types/aws-lambda": "^8.10.108",
"@ucanto/core": "^7.1.1",
"@ucanto/core": "^8.0.0",
"@web-std/blob": "3.0.4",
"@web3-storage/access": "^13.0.0",
"ava": "^4.3.3",
"aws-lambda-test-utils": "^1.3.0",
"nanoid": "^4.0.0",
Expand Down
155 changes: 155 additions & 0 deletions upload-api/stores/provisions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {
QueryCommand,
PutItemCommand,
DescribeTableCommand,
} from '@aws-sdk/client-dynamodb'
import { Failure } from '@ucanto/server'
import { marshall, } from '@aws-sdk/util-dynamodb'

import { ConflictError as ConsumerConflictError } from '../tables/consumer.js'
import { ConflictError as SubscriptionConflictError } from '../tables/subscription.js'

class ConflictError extends Failure {
/**
* @param {object} input
* @param {string} input.message
*/
constructor({ message }) {
super(message)
this.name = 'ConflictError'
}
}

/**
* @param {import('../types').SubscriptionTable} subscriptionTable
* @param {import('../types').ConsumerTable} consumerTable
* @param {import('@ucanto/interface').DID<'web'>[]} services
* @returns {import('@web3-storage/upload-api').ProvisionsStorage}
*/
export function useProvisionStore (subscriptionTable, consumerTable, services) {
return {
services,
hasStorageProvider: async (consumer) => (
{ ok: await consumerTable.hasStorageProvider(consumer) }
),

put: async (item) => {
const { cause, consumer, customer, provider } = item
// by setting subscription to customer we make it so each customer can have at most one subscription
// TODO is this what we want?
const subscription = customer

try {
await subscriptionTable.insert({
cause: cause.cid,
provider,
customer,
subscription
})
} catch (error) {
// if we got a conflict error, ignore - it means the subscription already exists and
// can be used to create a consumer/provider relationship below
if (!(error instanceof SubscriptionConflictError)) {
throw error
}
}

try {
await consumerTable.insert({
cause: cause.cid,
provider,
consumer,
subscription
})
return { ok: {} }
} catch (error) {
if (error instanceof ConsumerConflictError) {
return {
error
}
} else {
throw error
}
}
},

/**
* get number of stored items
*/
count: async () => {
return consumerTable.count()
}
}
}

/**
* @param {import('@aws-sdk/client-dynamodb').DynamoDBClient} dynamoDb
* @param {string} subscriptionsTableName
* @param {string} consumersTableName
* @param {import('@ucanto/interface').DID<'web'>[]} services
* @returns {import('@web3-storage/upload-api').ProvisionsStorage}
*/
export function useProvisionsStorage (dynamoDb, subscriptionsTableName, consumersTableName, services) {
return {
services,
hasStorageProvider: async (consumer) => {
const cmd = new QueryCommand({
TableName: consumersTableName,
KeyConditions: {
consumer: {
ComparisonOperator: 'EQ',
AttributeValueList: [{ S: consumer }]
}
},
AttributesToGet: ['cid']
})
const response = await dynamoDb.send(cmd)
const itemCount = response.Items?.length || 0
return { ok: itemCount > 0 }
},

put: async (item) => {
const row = {
cid: item.cause.cid.toString(),
consumer: item.consumer,
provider: item.provider,
customer: item.customer,
}
try {
await dynamoDb.send(new PutItemCommand({
TableName: subscriptionsTableName,
Item: marshall(row),
ConditionExpression: `attribute_not_exists(consumer) OR ((cid = :cid) AND (consumer = :consumer) AND (provider = :provider) AND (customer = :customer))`,
ExpressionAttributeValues: {
':cid': { 'S': row.cid },
':consumer': { 'S': row.consumer },
':provider': { 'S': row.provider },
':customer': { 'S': row.customer }
}
}))
} catch (error) {
if (error instanceof Error && error.message === 'The conditional request failed') {
return {
error: new ConflictError({
message: `Space ${row.consumer} cannot be provisioned with ${row.provider}: it already has a provider`
})
}
} else {
throw error
}
}
return { ok: {} }
},

/**
* get number of stored items
*/
count: async () => {
const result = await dynamoDb.send(new DescribeTableCommand({
TableName: consumersTableName
}))

return BigInt(result.Table?.ItemCount ?? -1)
}
}
}
Loading

0 comments on commit aafd061

Please sign in to comment.