feat(setting): add shared [redis] section as default for redis-backed subsystems (#38550)
Adds a shared `[redis]` config section with a single `CONN_STR` key that acts as the default Redis connection for every redis-backed subsystem whose own connection string is empty: `[cache].HOST`, `[session].PROVIDER_CONFIG`, `[queue].CONN_STR` and `[global_lock].SERVICE_CONN_STR`. Precedence: per-subsystem value > shared `[redis].CONN_STR` > existing built-in defaults. Fully backward compatible: when `[redis]` is not set, every code path behaves exactly as before. Motivation: today an admin with one Redis instance has to repeat the same connection string in up to four places (five once WebSocket lands). Requested by @wxiaoguang in [this review discussion](https://github.com/go-gitea/gitea/pull/36965#discussion_r3613306976) as a prerequisite for #36965. This follows the pattern used by GitLab and Mastodon: one global Redis definition, per-subsystem overrides fall back to it. Since #12385 all identical connection strings already share one client via `nosql.Manager`, so pointing all subsystems at `[redis].CONN_STR` naturally converges onto a single shared connection pool. Covered by table-driven unit tests per subsystem (fallback applies, own value wins, absent `[redis]` = unchanged behavior). A config-cheat-sheet update for gitea/docs will follow once this is merged. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
This commit is contained in:
@@ -1603,6 +1603,19 @@ LEVEL = Info
|
|||||||
;; If you'd like to enable it, you can set it to a value between 0 and 2.
|
;; If you'd like to enable it, you can set it to a value between 0 and 2.
|
||||||
;TYPE_BLEVE_MAX_FUZZINESS = 0
|
;TYPE_BLEVE_MAX_FUZZINESS = 0
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;[redis]
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;
|
||||||
|
;; Shared redis connection, used as the default for every redis-backed subsystem
|
||||||
|
;; (cache, session, queue, global lock, etc.) whose own connection string is left empty.
|
||||||
|
;; A per-subsystem connection value always takes precedence over this shared default.
|
||||||
|
;; When left empty, each subsystem keeps its previous behavior (no shared default).
|
||||||
|
;; e.g. `redis://127.0.0.1:6379/0` (or `redis+cluster://127.0.0.1:6379/0` for a Redis cluster)
|
||||||
|
;CONN_STR =
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;[queue]
|
;[queue]
|
||||||
@@ -1628,10 +1641,10 @@ LEVEL = Info
|
|||||||
;; Batch size to send for batched queues
|
;; Batch size to send for batched queues
|
||||||
;BATCH_LENGTH = 20
|
;BATCH_LENGTH = 20
|
||||||
;;
|
;;
|
||||||
;; Connection string for redis queues this will store the redis (or Redis cluster) connection string.
|
|
||||||
;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb
|
;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb
|
||||||
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
|
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
|
||||||
;CONN_STR = "redis://127.0.0.1:6379/0"
|
;; When `TYPE` is `redis` and this is left empty, it falls back to the shared [redis] CONN_STR.
|
||||||
|
;CONN_STR =
|
||||||
;;
|
;;
|
||||||
;; Provides the suffix of the default redis/disk queue name - specific queues can be overridden within in their [queue.name] sections.
|
;; Provides the suffix of the default redis/disk queue name - specific queues can be overridden within in their [queue.name] sections.
|
||||||
;QUEUE_NAME = "_queue"
|
;QUEUE_NAME = "_queue"
|
||||||
@@ -1929,7 +1942,7 @@ LEVEL = Info
|
|||||||
;INTERVAL = 60
|
;INTERVAL = 60
|
||||||
;;
|
;;
|
||||||
;; For "redis" and "memcache", connection host address
|
;; For "redis" and "memcache", connection host address
|
||||||
;; redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` (or `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` for a Redis cluster)
|
;; redis: default to shared [redis] CONN_STR
|
||||||
;; memcache: `127.0.0.1:11211`
|
;; memcache: `127.0.0.1:11211`
|
||||||
;; twoqueue: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000`
|
;; twoqueue: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000`
|
||||||
;HOST =
|
;HOST =
|
||||||
@@ -1965,10 +1978,10 @@ LEVEL = Info
|
|||||||
;;
|
;;
|
||||||
;; Provider config options
|
;; Provider config options
|
||||||
;; memory: doesn't have any config yet
|
;; memory: doesn't have any config yet
|
||||||
;; file: session file path, e.g. `data/sessions`
|
;; file: session file path, e.g. `data/sessions`, relative paths will be made absolute against _`AppWorkPath`_.
|
||||||
;; redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` (or `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` for a Redis cluster)
|
;; redis: default to [redis] CONN_STR
|
||||||
;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
|
;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
|
||||||
;PROVIDER_CONFIG = data/sessions ; Relative paths will be made absolute against _`AppWorkPath`_.
|
;PROVIDER_CONFIG =
|
||||||
;;
|
;;
|
||||||
;; Session cookie name
|
;; Session cookie name
|
||||||
;COOKIE_NAME = i_like_gitea
|
;COOKIE_NAME = i_like_gitea
|
||||||
@@ -3027,5 +3040,5 @@ LEVEL = Info
|
|||||||
;[global_lock]
|
;[global_lock]
|
||||||
;; Lock service type, could be memory or redis
|
;; Lock service type, could be memory or redis
|
||||||
;SERVICE_TYPE = memory
|
;SERVICE_TYPE = memory
|
||||||
;; Ignored for the "memory" type. For "redis" use something like `redis://127.0.0.1:6379/0`
|
;; Ignored for the "memory" type. For "redis", default to [redis] CONN_STR.
|
||||||
;SERVICE_CONN_STR =
|
;SERVICE_CONN_STR =
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package setting
|
package setting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
@@ -14,7 +13,7 @@ import (
|
|||||||
type Cache struct {
|
type Cache struct {
|
||||||
Adapter string
|
Adapter string
|
||||||
Interval int
|
Interval int
|
||||||
Conn string
|
Conn string `ini:"-"`
|
||||||
TTL time.Duration `ini:"ITEM_TTL"`
|
TTL time.Duration `ini:"ITEM_TTL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,13 +52,12 @@ func loadCacheFrom(rootCfg ConfigProvider) {
|
|||||||
CacheService.Adapter = sec.Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache", "twoqueue"})
|
CacheService.Adapter = sec.Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache", "twoqueue"})
|
||||||
switch CacheService.Adapter {
|
switch CacheService.Adapter {
|
||||||
case "memory":
|
case "memory":
|
||||||
case "redis", "memcache":
|
case "memcache":
|
||||||
CacheService.Conn = strings.Trim(sec.Key("HOST").String(), "\" ")
|
CacheService.Conn = sec.Key("HOST").String()
|
||||||
|
case "redis":
|
||||||
|
CacheService.Conn = sec.Key("HOST").MustString(Redis.ConnStr)
|
||||||
case "twoqueue":
|
case "twoqueue":
|
||||||
CacheService.Conn = strings.TrimSpace(sec.Key("HOST").String())
|
CacheService.Conn = sec.Key("HOST").MustString("50000")
|
||||||
if CacheService.Conn == "" {
|
|
||||||
CacheService.Conn = "50000"
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
log.Fatal("Unknown cache adapter: %s", CacheService.Adapter)
|
log.Fatal("Unknown cache adapter: %s", CacheService.Adapter)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCacheRedisSharedConnFallback(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
iniStr string
|
||||||
|
wantConn string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "redis adapter with empty HOST falls back to shared [redis]",
|
||||||
|
iniStr: `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[cache]
|
||||||
|
ADAPTER = redis
|
||||||
|
`,
|
||||||
|
wantConn: "redis://127.0.0.1:6379/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cache HOST wins over shared [redis]",
|
||||||
|
iniStr: `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[cache]
|
||||||
|
ADAPTER = redis
|
||||||
|
HOST = redis://10.0.0.1:6379/1
|
||||||
|
`,
|
||||||
|
wantConn: "redis://10.0.0.1:6379/1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no shared [redis] keeps previous behavior (empty conn)",
|
||||||
|
iniStr: `
|
||||||
|
[cache]
|
||||||
|
ADAPTER = redis
|
||||||
|
`,
|
||||||
|
wantConn: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "memcache adapter is never affected by shared [redis]",
|
||||||
|
iniStr: `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[cache]
|
||||||
|
ADAPTER = memcache
|
||||||
|
`,
|
||||||
|
wantConn: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&Redis)()
|
||||||
|
cfg, err := NewConfigProviderFromData(tt.iniStr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
loadRedisFrom(cfg)
|
||||||
|
loadCacheFrom(cfg)
|
||||||
|
assert.Equal(t, tt.wantConn, CacheService.Conn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ func loadGlobalLockFrom(rootCfg ConfigProvider) {
|
|||||||
switch GlobalLock.ServiceType {
|
switch GlobalLock.ServiceType {
|
||||||
case "memory":
|
case "memory":
|
||||||
case "redis":
|
case "redis":
|
||||||
connStr := sec.Key("SERVICE_CONN_STR").String()
|
connStr := sec.Key("SERVICE_CONN_STR").MustString(Redis.ConnStr)
|
||||||
if connStr == "" {
|
if connStr == "" {
|
||||||
log.Fatal("SERVICE_CONN_STR is empty for redis")
|
log.Fatal("SERVICE_CONN_STR is empty for redis")
|
||||||
}
|
}
|
||||||
@@ -6,20 +6,24 @@ package setting
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadGlobalLockConfig(t *testing.T) {
|
func TestLoadGlobalLockConfig(t *testing.T) {
|
||||||
t.Run("DefaultGlobalLockConfig", func(t *testing.T) {
|
t.Run("DefaultGlobalLockConfig", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&Redis)()
|
||||||
iniStr := ``
|
iniStr := ``
|
||||||
cfg, err := NewConfigProviderFromData(iniStr)
|
cfg, err := NewConfigProviderFromData(iniStr)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
loadRedisFrom(cfg)
|
||||||
loadGlobalLockFrom(cfg)
|
loadGlobalLockFrom(cfg)
|
||||||
assert.Equal(t, "memory", GlobalLock.ServiceType)
|
assert.Equal(t, "memory", GlobalLock.ServiceType)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("RedisGlobalLockConfig", func(t *testing.T) {
|
t.Run("RedisGlobalLockConfig", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&Redis)()
|
||||||
iniStr := `
|
iniStr := `
|
||||||
[global_lock]
|
[global_lock]
|
||||||
SERVICE_TYPE = redis
|
SERVICE_TYPE = redis
|
||||||
@@ -27,9 +31,44 @@ SERVICE_CONN_STR = addrs=127.0.0.1:6379 db=0
|
|||||||
`
|
`
|
||||||
cfg, err := NewConfigProviderFromData(iniStr)
|
cfg, err := NewConfigProviderFromData(iniStr)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
loadRedisFrom(cfg)
|
||||||
loadGlobalLockFrom(cfg)
|
loadGlobalLockFrom(cfg)
|
||||||
assert.Equal(t, "redis", GlobalLock.ServiceType)
|
assert.Equal(t, "redis", GlobalLock.ServiceType)
|
||||||
assert.Equal(t, "addrs=127.0.0.1:6379 db=0", GlobalLock.ServiceConnStr)
|
assert.Equal(t, "addrs=127.0.0.1:6379 db=0", GlobalLock.ServiceConnStr)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("RedisGlobalLockFallsBackToSharedRedis", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&Redis)()
|
||||||
|
iniStr := `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[global_lock]
|
||||||
|
SERVICE_TYPE = redis
|
||||||
|
`
|
||||||
|
cfg, err := NewConfigProviderFromData(iniStr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
loadRedisFrom(cfg)
|
||||||
|
loadGlobalLockFrom(cfg)
|
||||||
|
assert.Equal(t, "redis", GlobalLock.ServiceType)
|
||||||
|
assert.Equal(t, "redis://127.0.0.1:6379/0", GlobalLock.ServiceConnStr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RedisGlobalLockOwnConnWinsOverSharedRedis", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&Redis)()
|
||||||
|
iniStr := `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[global_lock]
|
||||||
|
SERVICE_TYPE = redis
|
||||||
|
SERVICE_CONN_STR = redis://10.0.0.1:6379/1
|
||||||
|
`
|
||||||
|
cfg, err := NewConfigProviderFromData(iniStr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
loadRedisFrom(cfg)
|
||||||
|
loadGlobalLockFrom(cfg)
|
||||||
|
assert.Equal(t, "redis", GlobalLock.ServiceType)
|
||||||
|
assert.Equal(t, "redis://10.0.0.1:6379/1", GlobalLock.ServiceConnStr)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ var (
|
|||||||
// AppWorkPath is the "working directory" of Gitea. It maps to the: WORK_PATH in app.ini, "--work-path" flag, environment variable GITEA_WORK_DIR.
|
// AppWorkPath is the "working directory" of Gitea. It maps to the: WORK_PATH in app.ini, "--work-path" flag, environment variable GITEA_WORK_DIR.
|
||||||
// If that is not set it is the default set here by the linker or failing that the directory of AppPath.
|
// If that is not set it is the default set here by the linker or failing that the directory of AppPath.
|
||||||
// It is used as the base path for several other paths.
|
// It is used as the base path for several other paths.
|
||||||
|
// Do remember:
|
||||||
|
// * Work path might not be the POSIX current working directory or the Gitea's binary directory, Gitea binary is not FHS-compliant
|
||||||
|
// * Work path sometimes is not writable (e.g.: /usr/local/bin) or not persistent (https://github.com/go-gitea/gitea/pull/35851)
|
||||||
|
// * Work path, custom path and data path, some of them are the same sometimes (e.g.: docker image, some downstream packages, some manual installations)
|
||||||
AppWorkPath string
|
AppWorkPath string
|
||||||
CustomPath string // Custom directory path. Env: GITEA_CUSTOM
|
CustomPath string // Custom directory path. Env: GITEA_CUSTOM
|
||||||
CustomConf string
|
CustomConf string
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/modules/json"
|
"gitea.dev/modules/json"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// QueueSettings represent the settings for a queue from the ini
|
// QueueSettings represent the settings for a queue from the ini
|
||||||
@@ -77,8 +78,10 @@ func GetQueueSettings(rootCfg ConfigProvider, name string) (QueueSettings, error
|
|||||||
}
|
}
|
||||||
cfg.Datadir = filepath.ToSlash(cfg.Datadir)
|
cfg.Datadir = filepath.ToSlash(cfg.Datadir)
|
||||||
|
|
||||||
|
// redis fallback order: [queue]/[queue.name] CONN_STR (already merged above) wins,
|
||||||
|
// then the shared [redis] conn, then the built-in localhost default for compatibility
|
||||||
if cfg.Type == "redis" && cfg.ConnStr == "" {
|
if cfg.Type == "redis" && cfg.ConnStr == "" {
|
||||||
cfg.ConnStr = "redis://127.0.0.1:6379/0"
|
cfg.ConnStr = util.IfZero(Redis.ConnStr, "redis://127.0.0.1:6379/0")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Length <= 0 {
|
if cfg.Length <= 0 {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQueueRedisSharedConnFallback(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
iniStr string
|
||||||
|
queueName string
|
||||||
|
wantConn string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "redis queue with empty CONN_STR falls back to shared [redis]",
|
||||||
|
iniStr: `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[queue]
|
||||||
|
TYPE = redis
|
||||||
|
`,
|
||||||
|
queueName: "test",
|
||||||
|
wantConn: "redis://127.0.0.1:6379/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base [queue] CONN_STR wins over shared [redis]",
|
||||||
|
iniStr: `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[queue]
|
||||||
|
TYPE = redis
|
||||||
|
CONN_STR = redis://10.0.0.1:6379/1
|
||||||
|
`,
|
||||||
|
queueName: "test",
|
||||||
|
wantConn: "redis://10.0.0.1:6379/1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "per-queue CONN_STR wins over shared [redis]",
|
||||||
|
iniStr: `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[queue]
|
||||||
|
TYPE = redis
|
||||||
|
[queue.test]
|
||||||
|
CONN_STR = redis://10.0.0.1:6379/2
|
||||||
|
`,
|
||||||
|
queueName: "test",
|
||||||
|
wantConn: "redis://10.0.0.1:6379/2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other queues still fall back to shared [redis] when only one is overridden",
|
||||||
|
iniStr: `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[queue]
|
||||||
|
TYPE = redis
|
||||||
|
[queue.test]
|
||||||
|
CONN_STR = redis://10.0.0.1:6379/2
|
||||||
|
`,
|
||||||
|
queueName: "other",
|
||||||
|
wantConn: "redis://127.0.0.1:6379/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no shared [redis] keeps previous behavior (built-in localhost default)",
|
||||||
|
iniStr: `
|
||||||
|
[queue]
|
||||||
|
TYPE = redis
|
||||||
|
`,
|
||||||
|
queueName: "test",
|
||||||
|
wantConn: "redis://127.0.0.1:6379/0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&Redis)()
|
||||||
|
cfg, err := NewConfigProviderFromData(tt.iniStr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
loadRedisFrom(cfg)
|
||||||
|
q, err := GetQueueSettings(cfg, tt.queueName)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.wantConn, q.ConnStr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
type RedisType struct {
|
||||||
|
ConnStr string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis represents the shared redis configuration. Its connection string is used
|
||||||
|
// as the default for every redis-backed subsystem (cache, session, queue, global
|
||||||
|
// lock) whose own connection string is empty. A per-subsystem value always wins.
|
||||||
|
var Redis *RedisType
|
||||||
|
|
||||||
|
func loadRedisFrom(rootCfg ConfigProvider) {
|
||||||
|
Redis = &RedisType{} // make sure the Redis config is correctly loaded before it is accessed by other subsystems
|
||||||
|
Redis.ConnStr = rootCfg.Section("redis").Key("CONN_STR").String()
|
||||||
|
}
|
||||||
@@ -42,13 +42,23 @@ var SessionConfig = struct {
|
|||||||
|
|
||||||
func loadSessionFrom(rootCfg ConfigProvider) {
|
func loadSessionFrom(rootCfg ConfigProvider) {
|
||||||
sec := rootCfg.Section("session")
|
sec := rootCfg.Section("session")
|
||||||
SessionConfig.Provider = sec.Key("PROVIDER").In("memory",
|
SessionConfig.Provider = sec.Key("PROVIDER").In("memory", []string{"memory", "file", "redis", "mysql", "postgres", "couchbase", "memcache", "db"})
|
||||||
[]string{"memory", "file", "redis", "mysql", "postgres", "couchbase", "memcache", "db"})
|
|
||||||
SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(filepath.Join(AppDataPath, "sessions")), "\" ")
|
switch SessionConfig.Provider {
|
||||||
if SessionConfig.Provider == "file" && !filepath.IsAbs(SessionConfig.ProviderConfig) {
|
case "redis":
|
||||||
SessionConfig.ProviderConfig = filepath.Join(AppWorkPath, SessionConfig.ProviderConfig)
|
SessionConfig.ProviderConfig = sec.Key("PROVIDER_CONFIG").MustString(Redis.ConnStr)
|
||||||
|
case "file":
|
||||||
|
SessionConfig.ProviderConfig = sec.Key("PROVIDER_CONFIG").MustString(filepath.Join(AppDataPath, "sessions"))
|
||||||
|
if !filepath.IsAbs(SessionConfig.ProviderConfig) {
|
||||||
|
// Although the "data path" should be used as Gitea's "data" base directory (work path sometimes is not writable),
|
||||||
|
// document says the relative session path is based on the "work path", so keep the behavior
|
||||||
|
SessionConfig.ProviderConfig = filepath.Join(AppWorkPath, SessionConfig.ProviderConfig)
|
||||||
|
}
|
||||||
checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig)
|
checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig)
|
||||||
|
default:
|
||||||
|
SessionConfig.ProviderConfig = sec.Key("PROVIDER_CONFIG").String()
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
|
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
|
||||||
// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath
|
// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath
|
||||||
SessionConfig.CookiePath = util.IfZero(AppSubURL, "/")
|
SessionConfig.CookiePath = util.IfZero(AppSubURL, "/")
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSessionRedisSharedConnFallback(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
iniStr string
|
||||||
|
wantContain string
|
||||||
|
wantMissing string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "redis provider with empty PROVIDER_CONFIG falls back to shared [redis]",
|
||||||
|
iniStr: `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[session]
|
||||||
|
PROVIDER = redis
|
||||||
|
`,
|
||||||
|
wantContain: "redis://127.0.0.1:6379/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "session PROVIDER_CONFIG wins over shared [redis]",
|
||||||
|
iniStr: `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[session]
|
||||||
|
PROVIDER = redis
|
||||||
|
PROVIDER_CONFIG = redis://10.0.0.1:6379/1
|
||||||
|
`,
|
||||||
|
wantContain: "redis://10.0.0.1:6379/1",
|
||||||
|
wantMissing: "127.0.0.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no shared [redis]",
|
||||||
|
iniStr: `
|
||||||
|
[session]
|
||||||
|
PROVIDER = redis
|
||||||
|
`,
|
||||||
|
wantContain: "",
|
||||||
|
wantMissing: "redis://",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file provider default path is untouched by shared [redis]",
|
||||||
|
iniStr: `
|
||||||
|
[redis]
|
||||||
|
CONN_STR = redis://127.0.0.1:6379/0
|
||||||
|
[session]
|
||||||
|
PROVIDER = file
|
||||||
|
`,
|
||||||
|
wantContain: "sessions",
|
||||||
|
wantMissing: "redis://",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&Redis)()
|
||||||
|
cfg, err := NewConfigProviderFromData(tt.iniStr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
loadRedisFrom(cfg)
|
||||||
|
loadSessionFrom(cfg)
|
||||||
|
// ProviderConfig is shadowed into a JSON blob at the end of loadSessionFrom
|
||||||
|
assert.Contains(t, SessionConfig.ProviderConfig, tt.wantContain)
|
||||||
|
if tt.wantMissing != "" {
|
||||||
|
assert.NotContains(t, SessionConfig.ProviderConfig, tt.wantMissing)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -156,6 +156,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
|||||||
loadGitFrom(cfg)
|
loadGitFrom(cfg)
|
||||||
loadMirrorFrom(cfg)
|
loadMirrorFrom(cfg)
|
||||||
loadMarkupFrom(cfg)
|
loadMarkupFrom(cfg)
|
||||||
|
loadRedisFrom(cfg)
|
||||||
loadGlobalLockFrom(cfg)
|
loadGlobalLockFrom(cfg)
|
||||||
loadOtherFrom(cfg)
|
loadOtherFrom(cfg)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user