Skip to content

dissolveGroup実行時にすべてのメンバーノードがdisconnected状態に遷移しない問題 #8

@takaokouji

Description

@takaokouji

問題の説明

現在の振る舞い

  • 2つのノードがグループに参加している
  • ホストノードがそのグループに対してdissolveGroupを実行する
  • もう一方のノードは変化せず、そのグループに参加し続けている状態になる

期待される振る舞い

  • dissolveGroupを実行すると、参加しているノードすべてがそれを検知してdisconnected状態に遷移する

現在の実装の分析

バックエンドの実装状況

dissolveGroup mutationは完全に実装済み

  • lambda/use_cases/dissolve_group.rb にて実装
  • DynamoDBから全てのグループデータを削除(メタデータ、ノード、ステータス)
  • ホスト権限の検証あり

onGroupDissolve subscriptionが自動的にトリガーされる

  • graphql/schema.graphql で定義
  • AppSyncが自動的にgroupIdとdomainでフィルタリングして購読者に通知
  • リアルタイムでメンバーに通知される仕組みは実装済み

既存のテストカバレッジ

  • Unit tests: spec/unit/use_cases/dissolve_group_spec.rb (88 lines)
  • Integration tests: spec/requests/dissolve_group_spec.rb (137 lines)
  • ただし、subscription通知のE2Eテストは未実装

問題の原因仮説

1. クライアント側の実装不足

  • フロントエンド(Smalruby3-GUI)が onGroupDissolve subscription を購読していない可能性
  • subscriptionを購読していても、通知受信時の状態更新処理が未実装の可能性

2. Subscriptionの信頼性問題

  • WebSocket接続が切れている場合のフォールバック機構がない
  • ネットワーク不安定時に通知が届かない可能性
  • クライアント側でsubscription接続の監視が不十分

提案する解決策

Option 1: クライアント側の実装確認・修正 ⭐️ 推奨

フロントエンド(Smalruby3-GUI)で以下を実装:

// onGroupDissolve subscriptionを購読
const subscription = API.graphql({
  query: onGroupDissolveSubscription,
  variables: { groupId, domain }
}).subscribe({
  next: (data) => {
    console.log('Group dissolved:', data);
    // disconnected状態に遷移
    updateNodeState('disconnected');
    // UIを更新
    showDisconnectedMessage('Group has been dissolved by host');
  },
  error: (error) => {
    console.error('Subscription error:', error);
  }
});

メリット:

  • リアルタイムで即座に反応
  • バックエンドの変更不要
  • AppSyncの機能を最大限活用

デメリット:

  • WebSocket切断時に通知が届かない可能性

Option 2: ポーリングによるフォールバック

定期的にグループの存在確認を実装:

// メインの通知はsubscriptionで受け取る
const subscription = subscribeToGroupDissolve(groupId, domain);

// フォールバック: 定期的にグループの存在確認
const pollInterval = setInterval(async () => {
  const group = await getGroup(groupId, domain);
  if (!group && currentState === 'connected') {
    console.log('Group no longer exists (detected by polling)');
    updateNodeState('disconnected');
    showDisconnectedMessage('Connection lost');
  }
}, 10000); // 10秒ごと

メリット:

  • subscription失敗時のフォールバックとして機能
  • より確実にdisconnected状態に遷移できる

デメリット:

  • ポーリング間隔分の遅延が発生
  • 不要なAPI呼び出しが増える

Option 3: バックエンドでの明示的な通知レコード

DynamoDBに各メンバー向けの通知レコードを作成し、クライアントが次回のデータ取得時に確実に検知できるようにする。

# lambda/use_cases/dissolve_group.rb
def execute(group_id:, domain:, host_id:)
  # ... existing code ...
  
  # Get all member nodes before dissolution
  members = @repository.find_group_members(group_id, domain)
  
  # Dissolve the group
  @repository.dissolve_group(group_id, domain)
  
  # Create notification records for each member
  members.each do |member|
    @repository.create_notification(
      node_id: member.node_id,
      type: 'GROUP_DISSOLVED',
      group_id: group_id,
      domain: domain,
      timestamp: Time.now.iso8601
    )
  end
  
  # ... existing code ...
end

メリット:

  • subscription失敗時でも確実に通知が届く
  • 通知履歴が残る

デメリット:

  • バックエンドの変更が必要
  • DynamoDBへの追加書き込みが発生
  • クライアント側で通知の取得・処理ロジックが必要

実装方針の推奨

推奨アプローチ: Option 1 + Option 2のハイブリッド

  1. Primary: onGroupDissolve subscriptionを実装(リアルタイム通知)
  2. Fallback: 定期的なポーリングを実装(信頼性向上)

これにより、以下を実現:

  • リアルタイム性を確保(subscription)
  • 信頼性を確保(polling fallback)
  • バックエンドの変更不要

テスト戦略

E2Eテストの実装(TDD)

新規テストファイル: spec/e2e/dissolve_group_notification_spec.rb

require 'spec_helper'

describe "dissolveGroup E2E notification test" do
  let(:domain) { "test.example.com" }
  let(:group_name) { "test-group-#{Time.now.to_i}" }
  let(:host_id) { "host-node-#{SecureRandom.uuid}" }
  let(:member_id) { "member-node-#{SecureRandom.uuid}" }

  it "2つのノードが接続後、dissolve groupで両方がdisconnected状態になる" do
    # Setup: Create group
    group = create_test_group(group_name, host_id, domain)
    group_id = group["id"]
    
    # Setup: Member joins group
    join_test_node(group_id, domain, member_id, "Member Node")
    
    # Setup: Both nodes subscribe to onGroupDissolve
    host_notifications = []
    member_notifications = []
    
    host_subscription = subscribe_to_group_dissolve(group_id, domain) do |data|
      host_notifications << data
    end
    
    member_subscription = subscribe_to_group_dissolve(group_id, domain) do |data|
      member_notifications << data
    end
    
    # Wait for subscriptions to be established
    sleep 2
    
    # Action: Host dissolves the group
    response = execute_graphql(dissolve_group_mutation, {
      groupId: group_id,
      domain: domain,
      hostId: host_id
    })
    
    expect(response["errors"]).to be_nil
    expect(response["data"]["dissolveGroup"]["groupId"]).to eq(group_id)
    
    # Wait for notifications to propagate
    sleep 3
    
    # Assertions: Both nodes should have received notification
    expect(host_notifications).not_to be_empty, "Host should receive dissolution notification"
    expect(member_notifications).not_to be_empty, "Member should receive dissolution notification"
    
    expect(host_notifications.first["groupId"]).to eq(group_id)
    expect(member_notifications.first["groupId"]).to eq(group_id)
    
    # Verify group is actually deleted
    get_response = execute_graphql(get_group_query, { groupId: group_id, domain: domain })
    expect(get_response["data"]["getGroup"]).to be_nil
    
    # Cleanup subscriptions
    host_subscription.unsubscribe
    member_subscription.unsubscribe
  end
  
  it "ホストのみがdissolveした場合、メンバーも通知を受け取る" do
    # Similar test focusing on member-only notification
  end
  
  it "subscription接続が切れている場合でも、ポーリングでdisconnected状態に遷移する" do
    # Test fallback polling mechanism (if implemented)
  end
end

テスト実装のステップ

  1. subscribe_to_group_dissolve ヘルパーメソッドを実装
  2. テストを実行して失敗することを確認(RED)
  3. フロントエンドでsubscription購読を実装
  4. テストが成功することを確認(GREEN)

実装ステップ

Phase 1: 現状確認

  • Smalruby3-GUIの現在のメッシュネットワーク実装を確認
  • GraphQLクライアントの実装を確認
  • 既存のsubscription実装があるか確認

Phase 2: バックエンドテストの実装(TDD)

  • spec/e2e/dissolve_group_notification_spec.rb を作成
  • WebSocket subscription用のテストヘルパーを実装
  • テストを実行して失敗することを確認(RED)

Phase 3: フロントエンド実装

  • onGroupDissolve subscriptionの購読実装
  • 通知受信時の状態遷移処理の実装
  • disconnected状態のUI表示を実装
  • エラーハンドリングの実装

Phase 4: フォールバック実装(Optional)

  • ポーリングによるグループ存在確認の実装
  • subscription失敗時のフォールバック処理

Phase 5: 検証

  • E2Eテストが成功することを確認(GREEN)
  • 手動テストで動作確認
  • ドキュメント更新

関連ファイル

Backend

  • lambda/use_cases/dissolve_group.rb - dissolveGroupの実装
  • graphql/schema.graphql - onGroupDissolve subscription定義
  • spec/requests/dissolve_group_spec.rb - 既存の統合テスト
  • SUBSCRIPTIONS.md - subscription仕様のドキュメント

Frontend(想定)

  • gui/smalruby3-gui/src/components/mesh-network/ - メッシュネットワーク関連コンポーネント
  • gui/smalruby3-gui/src/lib/mesh-network-client.js - GraphQL クライアント
  • gui/smalruby3-gui/src/lib/mesh-network-subscriptions.js - Subscription管理

期待される成果

  • ✅ ホストがdissolveGroupを実行した際、すべてのメンバーノードがリアルタイムで通知を受け取る
  • ✅ 通知を受け取ったノードは自動的にdisconnected状態に遷移する
  • ✅ UI上でグループ解散の通知が表示される
  • ✅ E2Eテストで動作が保証される
  • ✅ ネットワーク不安定時でもフォールバック機構により確実に状態遷移する

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions