Fix: concurrent plugin installation causes duplicate installations #216

Closed
opened 2026-02-16 00:20:31 -05:00 by yindo · 1 comment
Owner

Originally created by @Mairuis on GitHub (Dec 10, 2025).

Originally assigned to: @Stream29 on GitHub.

Problem

When installing a plugin via the management/install/identifiers API, if the same plugin installation request is sent concurrently (e.g., user clicks install button multiple times quickly), it creates multiple parallel installation tasks for the same plugin, resulting in duplicate installations.

Root Cause

The InstallMultiplePluginsToTenant and UpgradePlugin functions in internal/service/install_plugin.go lack concurrency control. The check for whether a plugin exists happens outside of any transaction or lock, leading to a classic TOCTOU (Time Of Check To Time Of Use) race condition:

Request A                           Request B
    |                                   |
    v                                   v
Check Plugin not exists ✓          Check Plugin not exists ✓  (both pass)
    |                                   |
    v                                   v
Create InstallTask_A               Create InstallTask_B
    |                                   |
    v                                   v
Start goroutine A                  Start goroutine B
    |                                   |
    v                                   v
Install plugin...                  Install plugin...

Current Solution (Temporary)

Add distributed locks (using Redis) before creating installation tasks to prevent concurrent installation of the same plugin for the same tenant.

  • Lock key format: plugin:install:{tenant_id}:{plugin_id}
  • Lock TTL: 10 minutes (auto-expire to prevent deadlock)
  • Lock acquire timeout: 100ms (fast fail)
  • Each job releases its own lock after completion

Files Changed

  • internal/service/install_plugin.go

Key Changes

  1. Acquire distributed lock before createInstallTasks
  2. Each goroutine releases its own lock after ProcessInstallJob completes
  3. Handle cache.Unlock errors with proper logging
  4. Return BadRequestError when lock acquisition fails (plugin installation already in progress)

Future Work

This is a temporary fix. The plugin installation logic needs to be refactored together with the API side to properly handle this case.

cc @Stream29

Originally created by @Mairuis on GitHub (Dec 10, 2025). Originally assigned to: @Stream29 on GitHub. ## Problem When installing a plugin via the `management/install/identifiers` API, if the same plugin installation request is sent concurrently (e.g., user clicks install button multiple times quickly), it creates multiple parallel installation tasks for the same plugin, resulting in duplicate installations. ## Root Cause The `InstallMultiplePluginsToTenant` and `UpgradePlugin` functions in `internal/service/install_plugin.go` lack concurrency control. The check for whether a plugin exists happens outside of any transaction or lock, leading to a classic TOCTOU (Time Of Check To Time Of Use) race condition: ``` Request A Request B | | v v Check Plugin not exists ✓ Check Plugin not exists ✓ (both pass) | | v v Create InstallTask_A Create InstallTask_B | | v v Start goroutine A Start goroutine B | | v v Install plugin... Install plugin... ``` ## Current Solution (Temporary) Add distributed locks (using Redis) before creating installation tasks to prevent concurrent installation of the same plugin for the same tenant. - Lock key format: `plugin:install:{tenant_id}:{plugin_id}` - Lock TTL: 10 minutes (auto-expire to prevent deadlock) - Lock acquire timeout: 100ms (fast fail) - Each job releases its own lock after completion ### Files Changed - `internal/service/install_plugin.go` ### Key Changes 1. Acquire distributed lock before `createInstallTasks` 2. Each goroutine releases its own lock after `ProcessInstallJob` completes 3. Handle `cache.Unlock` errors with proper logging 4. Return `BadRequestError` when lock acquisition fails (plugin installation already in progress) ## Future Work This is a temporary fix. The plugin installation logic needs to be refactored together with the API side to properly handle this case. cc @Stream29
yindo added the bug label 2026-02-16 00:20:31 -05:00
yindo closed this issue 2026-02-16 00:20:31 -05:00
Author
Owner

@Mairuis commented on GitHub (Dec 10, 2025):

This is a temporary solution. The plugin installation logic needs to be refactored together with the Dify API side in the future.

cc @Stream29

@Mairuis commented on GitHub (Dec 10, 2025): This is a temporary solution. The plugin installation logic needs to be refactored together with the Dify API side in the future. cc @Stream29
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langgenius/dify-plugin-daemon#216