diff --git a/CLAUDE.md b/CLAUDE.md index 53c996e..2b55c09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,11 +88,11 @@ Mesh v2 uses environment variables for configuration, allowing different setting | Variable | Development | Production | Description | |----------|-------------|------------|-------------| | `MESH_SECRET_KEY` | `dev-secret-key-for-testing` | (set in GitHub Secrets) | Secret key for domain validation | -| `MESH_HOST_HEARTBEAT_INTERVAL_SECONDS` | `15` | `30` | Host heartbeat interval in seconds | -| `MESH_HOST_HEARTBEAT_TTL_SECONDS` | `60` | `150` | Host group TTL in seconds (5× interval) | +| `MESH_HOST_HEARTBEAT_INTERVAL_SECONDS` | `15` | `60` | Host heartbeat interval in seconds | +| `MESH_HOST_HEARTBEAT_TTL_SECONDS` | `60` | `150` | Host group TTL in seconds | | `MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS` | `15` | `120` | Member heartbeat interval in seconds | -| `MESH_MEMBER_HEARTBEAT_TTL_SECONDS` | `60` | `600` | Member node TTL in seconds (5× interval) | -| `MESH_MAX_CONNECTION_TIME_MINUTES` | `10` | `50` | Maximum connection time for a group (minutes) | +| `MESH_MEMBER_HEARTBEAT_TTL_SECONDS` | `60` | `600` | Member node TTL in seconds | +| `MESH_MAX_CONNECTION_TIME_MINUTES` | `5` | `25` | Maximum connection time for a group (minutes) | ### Setup for Local Development @@ -118,7 +118,7 @@ npx cdk deploy --context stage=stg **Command Line Override**: ```bash -MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=30 \ +MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=60 \ MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS=120 \ npx cdk deploy --context stage=prod ``` diff --git a/docs/api-reference.md b/docs/api-reference.md index 1348720..1b93510 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -351,7 +351,7 @@ mutation RenewHeartbeat($groupId: ID!, $domain: String!, $hostId: ID!) { **重要**: この mutation はホストのみが実行できます。非ホストが実行すると `Unauthorized` エラーが返されます。 -**ハートビート間隔**: 環境変数 `MESH_HOST_HEARTBEAT_INTERVAL_SECONDS` で設定(開発環境: 15秒、本番環境: 30秒) +**ハートビート間隔**: 環境変数 `MESH_HOST_HEARTBEAT_INTERVAL_SECONDS` で設定(開発環境: 15秒、本番環境: 60秒) --- diff --git a/docs/deployment.md b/docs/deployment.md index 90336e9..4c8f21c 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -74,11 +74,11 @@ cp .env.example .env | 変数名 | 開発環境推奨値 | 本番環境推奨値 | 説明 | |--------|--------------|--------------|------| | `MESH_SECRET_KEY` | `dev-secret-key-for-testing` | (GitHub Secretsで設定) | ドメイン検証用の秘密鍵 | -| `MESH_HOST_HEARTBEAT_INTERVAL_SECONDS` | `15` | `30` | ホストのハートビート送信間隔(秒) | +| `MESH_HOST_HEARTBEAT_INTERVAL_SECONDS` | `15` | `60` | ホストのハートビート送信間隔(秒) | | `MESH_HOST_HEARTBEAT_TTL_SECONDS` | `60` | `150` | ホストグループの有効期限(秒) | | `MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS` | `15` | `120` | メンバーのハートビート送信間隔(秒) | | `MESH_MEMBER_HEARTBEAT_TTL_SECONDS` | `60` | `600` | メンバーノードの有効期限(秒) | -| `MESH_MAX_CONNECTION_TIME_SECONDS` | `300` | `3000` | グループの最大接続時間(秒) | +| `MESH_MAX_CONNECTION_TIME_SECONDS` | `300` | `1500` | グループの最大接続時間(秒) | ### 3.3 開発環境用の設定(stg) @@ -101,11 +101,11 @@ MESH_MAX_CONNECTION_TIME_SECONDS=300 ```bash # 本番環境ではGitHub Secretsまたは環境変数で設定 MESH_SECRET_KEY=<本番用の秘密鍵> -MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=30 +MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=60 MESH_HOST_HEARTBEAT_TTL_SECONDS=150 MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS=120 MESH_MEMBER_HEARTBEAT_TTL_SECONDS=600 -MESH_MAX_CONNECTION_TIME_SECONDS=3000 +MESH_MAX_CONNECTION_TIME_SECONDS=1500 ``` **重要**: @@ -175,10 +175,11 @@ npx cdk deploy ```bash # 環境変数を直接指定してデプロイ MESH_SECRET_KEY="<本番用秘密鍵>" \ -MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=30 \ +MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=60 \ MESH_HOST_HEARTBEAT_TTL_SECONDS=150 \ MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS=120 \ MESH_MEMBER_HEARTBEAT_TTL_SECONDS=600 \ +MESH_MAX_CONNECTION_TIME_SECONDS=1500 \ npx cdk deploy --context stage=prod ``` diff --git a/docs/development.md b/docs/development.md index f5f8555..4c2dace 100644 --- a/docs/development.md +++ b/docs/development.md @@ -138,7 +138,7 @@ Mesh v2 は環境変数を使用して設定を管理し、開発環境と本番 **本番環境(遅い間隔)**: - コスト最適化(~70% のコスト削減) - メンバーのハートビート 120 秒により、UX を維持しつつ API 呼び出しを削減 -- ホストのハートビート 30 秒により、グループ解散の検出を迅速化 +- ホストのハートビート 60 秒により、グループ解散の検出を迅速化 - TTL を間隔の 5 倍にすることで、ネットワークの一時的な問題に対応 ### 環境変数の使用方法 diff --git a/js/functions/checkExistingGroup.js b/js/functions/checkExistingGroup.js index 30bcc6d..10a1c16 100644 --- a/js/functions/checkExistingGroup.js +++ b/js/functions/checkExistingGroup.js @@ -5,6 +5,10 @@ import { util } from '@aws-appsync/utils'; export function request(ctx) { const { hostId, domain } = ctx.args; + const nowEpoch = Math.floor(util.time.nowEpochMilliSeconds() / 1000); + const ttlSeconds = +(ctx.env.MESH_HOST_HEARTBEAT_TTL_SECONDS || '150'); + const threshold = nowEpoch - ttlSeconds; + const nowISO = util.time.nowISO8601(); // 既存グループの検索 return { @@ -17,9 +21,11 @@ export function request(ctx) { }) }, filter: { - expression: 'hostId = :hostId', + expression: 'hostId = :hostId AND heartbeatAt > :threshold AND expiresAt > :now', expressionValues: util.dynamodb.toMapValues({ - ':hostId': hostId + ':hostId': hostId, + ':threshold': threshold, + ':now': nowISO }) } }; diff --git a/js/functions/checkGroupExists.js b/js/functions/checkGroupExists.js index 9890ef6..ba63b97 100644 --- a/js/functions/checkGroupExists.js +++ b/js/functions/checkGroupExists.js @@ -19,7 +19,8 @@ export function request(ctx) { export function response(ctx) { const { groupId, domain } = ctx.args; const nowEpoch = Math.floor(util.time.nowEpochMilliSeconds() / 1000); - const heartbeatThreshold = nowEpoch - 60; // heartbeat閾値: 60秒 + const ttlSeconds = +(ctx.env.MESH_HOST_HEARTBEAT_TTL_SECONDS || '150'); + const heartbeatThreshold = nowEpoch - ttlSeconds; // グループが存在しない if (!ctx.result) { diff --git a/js/functions/createGroupIfNotExists.js b/js/functions/createGroupIfNotExists.js index 20aa21a..4a011ff 100644 --- a/js/functions/createGroupIfNotExists.js +++ b/js/functions/createGroupIfNotExists.js @@ -24,7 +24,7 @@ export function request(ctx) { } // maxConnectionTimeSeconds のバリデーションと決定 - const envMaxSeconds = +(ctx.env.MESH_MAX_CONNECTION_TIME_SECONDS || '3000'); + const envMaxSeconds = +(ctx.env.MESH_MAX_CONNECTION_TIME_SECONDS || '1500'); let actualMaxSeconds = envMaxSeconds; if (maxConnectionTimeSeconds !== undefined && maxConnectionTimeSeconds !== null) { @@ -46,7 +46,8 @@ export function request(ctx) { const now = util.time.nowISO8601(); const nowEpoch = Math.floor(util.time.nowEpochMilliSeconds() / 1000); const expiresAt = util.time.epochMilliSecondsToISO8601(util.time.nowEpochMilliSeconds() + actualMaxSeconds * 1000); - const ttl = nowEpoch + 60; // 1分間 + const ttlSeconds = +(ctx.env.MESH_HOST_HEARTBEAT_TTL_SECONDS || '150'); + const ttl = nowEpoch + ttlSeconds; return { operation: 'PutItem', diff --git a/js/functions/findNodeMetadata.js b/js/functions/findNodeMetadata.js index e158095..eaafe38 100644 --- a/js/functions/findNodeMetadata.js +++ b/js/functions/findNodeMetadata.js @@ -20,11 +20,14 @@ export function response(ctx) { util.error(ctx.error.message, ctx.error.type); } - if (!ctx.result) { + const result = ctx.result; + const nowEpoch = Math.floor(util.time.nowEpochMilliSeconds() / 1000); + + if (!result || (result.ttl && result.ttl <= nowEpoch)) { return null; } // stashに保存して次のFunctionへ - ctx.stash.nodeMetadata = ctx.result; - return ctx.result; + ctx.stash.nodeMetadata = result; + return result; } diff --git a/js/functions/renewHeartbeatFunction.js b/js/functions/renewHeartbeatFunction.js index ddc73a7..5705cb4 100644 --- a/js/functions/renewHeartbeatFunction.js +++ b/js/functions/renewHeartbeatFunction.js @@ -48,6 +48,6 @@ export function response(ctx) { groupId: groupId, domain: domain, expiresAt: group.expiresAt, - heartbeatIntervalSeconds: +(ctx.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS || '30') + heartbeatIntervalSeconds: +(ctx.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS || '60') }; } diff --git a/js/resolvers/Mutation.createGroup.js b/js/resolvers/Mutation.createGroup.js index dd5e749..24d0dac 100644 --- a/js/resolvers/Mutation.createGroup.js +++ b/js/resolvers/Mutation.createGroup.js @@ -12,7 +12,7 @@ export function request(ctx) { } // maxConnectionTimeSeconds のバリデーションと決定 - const envMaxSeconds = +(ctx.env.MESH_MAX_CONNECTION_TIME_SECONDS || '3000'); + const envMaxSeconds = +(ctx.env.MESH_MAX_CONNECTION_TIME_SECONDS || '1500'); let actualMaxSeconds = envMaxSeconds; if (maxConnectionTimeSeconds !== undefined && maxConnectionTimeSeconds !== null) { @@ -34,7 +34,8 @@ export function request(ctx) { const now = util.time.nowISO8601(); const nowEpoch = Math.floor(util.time.nowEpochMilliSeconds() / 1000); const expiresAt = util.time.epochMilliSecondsToISO8601(util.time.nowEpochMilliSeconds() + actualMaxSeconds * 1000); - const ttl = nowEpoch + 60; // 1分間 + const ttlSeconds = +(ctx.env.MESH_HOST_HEARTBEAT_TTL_SECONDS || '150'); + const ttl = nowEpoch + ttlSeconds; return { operation: 'PutItem', diff --git a/js/resolvers/Mutation.reportDataByNode.js b/js/resolvers/Mutation.reportDataByNode.js index 3ed3bf8..9dd7d10 100644 --- a/js/resolvers/Mutation.reportDataByNode.js +++ b/js/resolvers/Mutation.reportDataByNode.js @@ -13,6 +13,10 @@ export function request(ctx) { value: item.value })); + const ttlSeconds = +(ctx.env.MESH_MEMBER_HEARTBEAT_TTL_SECONDS || '600'); + const nowEpoch = Math.floor(util.time.nowEpochMilliSeconds() / 1000); + const ttl = nowEpoch + ttlSeconds; + return { operation: 'PutItem', key: util.dynamodb.toMapValues({ @@ -25,6 +29,7 @@ export function request(ctx) { domain: domain, data: sensorDataList, timestamp: now, + ttl: ttl, // GSI用属性 gsi_pk: `NODE#${nodeId}`, gsi_sk: 'STATUS' diff --git a/js/resolvers/Query.listGroupStatuses.js b/js/resolvers/Query.listGroupStatuses.js index 9411f5b..24eedec 100644 --- a/js/resolvers/Query.listGroupStatuses.js +++ b/js/resolvers/Query.listGroupStatuses.js @@ -10,6 +10,7 @@ export function request(ctx) { if (!domain || !groupId) { util.error('groupId and domain are required', 'ValidationError'); } + const nowEpoch = Math.floor(util.time.nowEpochMilliSeconds() / 1000); // DynamoDB Query: DOMAIN#${domain} 配下の GROUP#${groupId}#NODE#*#STATUS を取得 return { @@ -20,6 +21,15 @@ export function request(ctx) { ':pk': `DOMAIN#${domain}`, ':sk_prefix': `GROUP#${groupId}#NODE#` }) + }, + filter: { + expression: 'attribute_not_exists(#ttl) OR #ttl > :now', + expressionNames: { + '#ttl': 'ttl' + }, + expressionValues: util.dynamodb.toMapValues({ + ':now': nowEpoch + }) } }; } diff --git a/js/resolvers/Query.listGroupsByDomain.js b/js/resolvers/Query.listGroupsByDomain.js index c72ee91..a48f5e0 100644 --- a/js/resolvers/Query.listGroupsByDomain.js +++ b/js/resolvers/Query.listGroupsByDomain.js @@ -6,7 +6,8 @@ import { util } from '@aws-appsync/utils'; export function request(ctx) { const { domain } = ctx.args; const nowEpoch = Math.floor(util.time.nowEpochMilliSeconds() / 1000); - const threshold = nowEpoch - 60; // 1分前 + const ttlSeconds = +(ctx.env.MESH_HOST_HEARTBEAT_TTL_SECONDS || '150'); + const threshold = nowEpoch - ttlSeconds; return { operation: 'Query', diff --git a/js/resolvers/Query.listNodesInGroup.js b/js/resolvers/Query.listNodesInGroup.js index bb7cb4f..16fd55f 100644 --- a/js/resolvers/Query.listNodesInGroup.js +++ b/js/resolvers/Query.listNodesInGroup.js @@ -5,6 +5,7 @@ import { util } from '@aws-appsync/utils'; export function request(ctx) { const { groupId, domain } = ctx.args; + const nowEpoch = Math.floor(util.time.nowEpochMilliSeconds() / 1000); return { operation: 'Query', @@ -14,6 +15,15 @@ export function request(ctx) { ':pk': `DOMAIN#${domain}`, ':sk_prefix': `GROUP#${groupId}#NODE#` }) + }, + filter: { + expression: 'attribute_not_exists(#ttl) OR #ttl > :now', + expressionNames: { + '#ttl': 'ttl' + }, + expressionValues: util.dynamodb.toMapValues({ + ':now': nowEpoch + }) } }; } diff --git a/lib/mesh-v2-stack.ts b/lib/mesh-v2-stack.ts index 3fa5c48..8662d4f 100644 --- a/lib/mesh-v2-stack.ts +++ b/lib/mesh-v2-stack.ts @@ -71,7 +71,7 @@ export class MeshV2Stack extends cdk.Stack { }); // Environment variables defaults based on stage - const defaultMaxConnTimeSeconds = stage === 'prod' ? '3000' : '600'; + const defaultMaxConnTimeSeconds = stage === 'prod' ? '1500' : '300'; // AppSync GraphQL API for Mesh v2 this.api = new appsync.GraphqlApi(this, 'MeshV2Api', { @@ -87,7 +87,7 @@ export class MeshV2Stack extends cdk.Stack { }, environmentVariables: { TABLE_NAME: this.table.tableName, - MESH_HOST_HEARTBEAT_INTERVAL_SECONDS: process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS || '30', + MESH_HOST_HEARTBEAT_INTERVAL_SECONDS: process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS || '60', MESH_HOST_HEARTBEAT_TTL_SECONDS: process.env.MESH_HOST_HEARTBEAT_TTL_SECONDS || '150', MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS: process.env.MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS || '120', MESH_MEMBER_HEARTBEAT_TTL_SECONDS: process.env.MESH_MEMBER_HEARTBEAT_TTL_SECONDS || '600', @@ -367,7 +367,7 @@ export class MeshV2Stack extends cdk.Stack { LC_ALL: 'en_US.UTF-8', DYNAMODB_TABLE_NAME: this.table.tableName, MESH_SECRET_KEY: process.env.MESH_SECRET_KEY || 'default-secret-key', - MESH_HOST_HEARTBEAT_INTERVAL_SECONDS: process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS || '30', + MESH_HOST_HEARTBEAT_INTERVAL_SECONDS: process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS || '60', MESH_HOST_HEARTBEAT_TTL_SECONDS: process.env.MESH_HOST_HEARTBEAT_TTL_SECONDS || '150', MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS: process.env.MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS || '120', MESH_MEMBER_HEARTBEAT_TTL_SECONDS: process.env.MESH_MEMBER_HEARTBEAT_TTL_SECONDS || '600',