feat(cache): cache the tenant model provider list in the daemon - #780
Merged
Conversation
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
force-pushed
the
feat/esq1-190-model-providers-cache
branch
from
July 27, 2026 08:56
a752697 to
dce4300
Compare
This was referenced Jul 27, 2026
GareArc
marked this pull request as ready for review
July 27, 2026 10:27
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. |
Contributor
There was a problem hiding this comment.
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
ListModelsto 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
[]Tvalues can round-trip viacache.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.
…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.
wylswz
approved these changes
Jul 28, 2026
Contributor
📄 Knowledge review✏️ Documentation updates1 page was updated by changes in this PR.
📝 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: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_ENABLEDcan be turned off.What changed
ListModelsnow reads throughhelper.CombinedListModelInstallations, mirroring the existingCombinedGetPluginDeclaration/DeletePluginDeclarationCachepair incombined.go.<page>:<pageSize>, so oneDELdrops every cached page — noSCAN, no version counter.InstallPlugin/UninstallPlugin/UpgradePlugininvalidate it, guarded on the samedeclaration.Model != nilcondition that already guards theAIModelInstallationrow writes.deferred rather than run after the transaction. A retried install aborts onErrPluginAlreadyInstalledbefore reaching any post-transaction code, so aDELplaced there becomes unreachable once it has failed once.PLUGIN_MODEL_INSTALLATIONS_CACHE_TTL. It is a safety net behind explicit invalidation, not the primary mechanism.Separate first commit
parser.UnmarshalJsoncould never return a slice:validator.Structrejects any non-struct top-level value, so it always errored before returning, which madecache.GetMapField[[]T]unusable. Slices now skip validation alongside maps and strings.UnmarshalJsonBytes2Sliceremains the variant that validates per element.Type of Change
Essential Checklist
Testing
pkg/utils/cache/helperandpkg/utils/parsertests pass locally. Theinternal/types/models/curdtests require postgres and redis viaTestMain, 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.godeletesAIModelInstallationrows 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.DEL. Bounded by the TTL. Same exposure as the existing declaration cache.