Skip to content

feat(cache): cache the tenant model provider list in the daemon - #780

Merged
GareArc merged 5 commits into
mainfrom
feat/esq1-190-model-providers-cache
Jul 28, 2026
Merged

feat(cache): cache the tenant model provider list in the daemon#780
GareArc merged 5 commits into
mainfrom
feat/esq1-190-model-providers-cache

Conversation

@GareArc

@GareArc GareArc commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Description

Plugins assigned to a workspace from the enterprise admin console never appear in that workspace's model provider list (ESQ1-190, Zendesk #3547).

Dify caches the per-tenant model provider list in Redis, but enterprise installs plugins by calling this daemon directly, bypassing Dify's API. Dify therefore never learns its own cache is stale, and the newly assigned plugin stays invisible until the TTL expires. A cache is only correct if the system that owns the writes can invalidate it — so the cache moves here.

This is phase 3 of the plan. Phase 2 (langgenius/dify#39632) added flags to disable Dify's caches; this builds the daemon-side replacement so PLUGIN_MODEL_PROVIDERS_CACHE_ENABLED can be turned off.

What changed

  • ListModels now reads through helper.CombinedListModelInstallations, mirroring the existing CombinedGetPluginDeclaration / DeletePluginDeclarationCache pair in combined.go.
  • Storage is a Redis hash per tenant, field <page>:<pageSize>, so one DEL drops every cached page — no SCAN, no version counter.
  • InstallPlugin / UninstallPlugin / UpgradePlugin invalidate it, guarded on the same declaration.Model != nil condition that already guards the AIModelInstallation row writes.
  • The invalidation is deferred rather than run after the transaction. A retried install aborts on ErrPluginAlreadyInstalled before reaching any post-transaction code, so a DEL placed there becomes unreachable once it has failed once.
  • TTL defaults to 60 minutes, configurable via PLUGIN_MODEL_INSTALLATIONS_CACHE_TTL. It is a safety net behind explicit invalidation, not the primary mechanism.

Separate first commit

parser.UnmarshalJson could never return a slice: validator.Struct rejects any non-struct top-level value, so it always errored before returning, which made cache.GetMapField[[]T] unusable. Slices now skip validation alongside maps and strings. UnmarshalJsonBytes2Slice remains the variant that validates per element.

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Performance improvement
  • Other

Essential Checklist

Testing

  • I have tested the changes locally and confirmed they work as expected
  • I have added unit tests where necessary and they pass successfully

pkg/utils/cache/helper and pkg/utils/parser tests pass locally. The internal/types/models/curd tests require postgres and redis via TestMain, so they run in CI but were not executed locally.

Additional Information

The model schema cache has the same ownership defect but a much narrower blast radius, and it is not what #3547 reports. It moves in a follow-up: #781.

Known and deliberate here:

  • cmd/commandline/plugin/cleanup.go deletes AIModelInstallation rows with no invalidation. It initialises only the DB, never Redis, so it cannot invalidate at all — after a manual cleanup run, deleted providers linger for up to the TTL. Consistent with how that CLI already bypasses every other daemon cache.
  • Read-repopulate race: a reader holding pre-commit rows can write them back after the DEL. Bounded by the TTL. Same exposure as the existing declaration cache.
  • Inert in production until the Dify-side flag is turned off.

GareArc added 2 commits July 27, 2026 01:21
validator.Struct rejects any non-struct top-level value, so UnmarshalJson
with a slice T always failed before returning. Skip validation for slices
alongside maps and strings.

UnmarshalJsonBytes2Slice remains the variant that validates per element.
Dify caches the per-tenant model provider list, but enterprise installs
plugins through the daemon directly, so Dify never learns its cache is
stale and a newly assigned plugin stays invisible until the TTL expires.

Move the cache to where the writes happen. ListModels now reads through a
Redis hash keyed per tenant, with the page as the field so a single DEL
drops every cached page, and InstallPlugin/UninstallPlugin/UpgradePlugin
invalidate it.

The invalidation is deferred rather than run after the transaction: a
retried install aborts on ErrPluginAlreadyInstalled before reaching any
post-transaction code, so a DEL placed there is unreachable once it has
failed once.

TTL defaults to 60 minutes, configurable via
PLUGIN_MODEL_INSTALLATIONS_CACHE_TTL.
@GareArc GareArc changed the title feat(cache): cache tenant model installations in the daemon feat(cache): move the model provider and schema caches into the daemon Jul 27, 2026
@GareArc
GareArc force-pushed the feat/esq1-190-model-providers-cache branch from a752697 to dce4300 Compare July 27, 2026 08:56
@GareArc GareArc changed the title feat(cache): move the model provider and schema caches into the daemon feat(cache): cache the tenant model provider list in the daemon Jul 27, 2026
@GareArc
GareArc marked this pull request as ready for review July 27, 2026 10:27
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. go Pull requests that update go code plugin-daemon labels Jul 27, 2026
@GareArc
GareArc requested a review from wylswz July 27, 2026 11:27
@wylswz

wylswz commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

I suggest disabling this cache by default, otherwise the cache size is doubled, i.e., another scaling up needs to be scheduled before deploying to cloud production env.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR moves ownership of the per-tenant “model provider list” cache into the plugin daemon so enterprise-side installs/upgrades/uninstalls can deterministically invalidate cached model-provider listings, avoiding stale results in Dify’s UI when Dify’s own cache is bypassed.

Changes:

  • Added a daemon-side Redis hash cache for tenant model-installation list pages and a helper to serve DB reads through that cache.
  • Wired ListModels to use the new cached list helper, and invalidated the cache on install/uninstall/upgrade when model declarations are involved.
  • Updated JSON parsing to skip entity validation for slice top-level values so cached []T values can round-trip via cache.GetMapField.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pkg/utils/parser/json.go Allows unmarshalling slices without struct validation so cache.GetMapField[[]T] can work.
pkg/utils/cache/helper/model_installations.go Implements tenant model-installation list cache (paged fields in a Redis hash) with TTL and invalidation helper.
pkg/utils/cache/helper/model_installations_test.go Unit tests for cache hit behavior, page separation, and invalidation.
pkg/utils/cache/helper/keys.go Adds a tenant-scoped cache key builder for model installations.
internal/types/models/curd/model_installations_cache_test.go Validates model-installations cache invalidation behavior during install retry scenarios.
internal/types/models/curd/atomic.go Invalidates the tenant model-installations cache during install/uninstall/upgrade when model declarations are present.
internal/types/app/default.go Introduces a default config value for the model-installations cache TTL (minutes).
internal/types/app/config.go Adds the PLUGIN_MODEL_INSTALLATIONS_CACHE_TTL config field.
internal/service/manage_plugin.go Switches ListModels to use the cached helper-based listing.
internal/core/plugin_manager/manager.go Applies configured TTL to the helper at daemon startup.
.env.example Documents/exposes the new TTL environment variable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/types/app/default.go
Comment thread .env.example
…y default

The daemon cache holds the same tenant payload dify already caches, so leaving
both on doubles the redis footprint and forces a scale-up before a cloud
production rollout. PLUGIN_MODEL_INSTALLATIONS_CACHE_ENABLED defaults to false;
enable it only where dify runs with PLUGIN_MODEL_PROVIDERS_CACHE_ENABLED=false.
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jul 28, 2026
@GareArc
GareArc merged commit 0b66639 into main Jul 28, 2026
7 checks passed
@GareArc
GareArc deleted the feat/esq1-190-model-providers-cache branch July 28, 2026 09:01
@dosubot

dosubot Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📄 Knowledge review

✏️ Documentation updates

1 page was updated by changes in this PR.

Page Library Status
cache /dify-plugin-daemon/blob/main/docs/claude/cache.md dify-plugin-sdks ✅ Updated
📝 cache — changes
@@ -98,6 +98,39 @@
 }
 ```
 
+## Model Installations Cache
+
+The daemon caches per-tenant model provider installation lists in Redis to reduce database queries. The cache is automatically invalidated when plugins with model providers are installed, upgraded, or uninstalled.
+
+### Cache Key Pattern
+
+`model_installations:tenant_id:{tenant_id}` - Redis hash map where each field represents a paginated query result (field format: `{page}:{page_size}`).
+
+### Configuration
+
+Two environment variables control this feature:
+
+- `PLUGIN_MODEL_INSTALLATIONS_CACHE_ENABLED` (default: false) - Enables the cache. Should only be enabled when Dify's own `PLUGIN_MODEL_PROVIDERS_CACHE_ENABLED` is false to avoid duplicating cached data in Redis.
+- `PLUGIN_MODEL_INSTALLATIONS_CACHE_TTL` (default: 1440 minutes / 24 hours) - How long cached data survives without explicit invalidation.
+
+### Helper Functions
+
+```go
+// Retrieves model installations from cache or DB, automatically populating cache on miss
+installations, err := helper.CombinedListModelInstallations(tenantId, page, pageSize)
+
+// Invalidates the entire cache for a tenant
+helper.DeleteModelInstallationsCache(tenantId)
+```
+
+### Automatic Invalidation
+
+The cache is automatically cleared when:
+
+- A plugin with model provider declaration is installed
+- A plugin with model provider declaration is uninstalled
+- A plugin is upgraded (if either old or new version has model provider declaration)
+
 ## Configuration
 
 Initialize Redis client in main:

Leave Feedback Ask Dosu about dify-plugin-daemon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go Pull requests that update go code lgtm This PR has been approved by a maintainer plugin-daemon size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants