@atomservice/redis 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +32 -0
- package/src/index.ts +3 -0
- package/src/redis.compose.ts +40 -0
- package/src/redis.consts.ts +2 -0
- package/src/redis.instance.ts +25 -0
- package/src/redis.options.ts +58 -0
- package/src/redis.service.ts +100 -0
- package/src/redis.types.ts +4 -0
- package/src/redis.utils.ts +16 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atomservice/redis",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Redis 原子服务",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "openorson",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/openorson/atomservice.git",
|
|
10
|
+
"directory": "services/redis"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/openorson/atomservice/issues",
|
|
13
|
+
"homepage": "https://github.com/openorson/atomservice/tree/main/services/redis#readme",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"atomservice",
|
|
16
|
+
"redis",
|
|
17
|
+
"self-hosted",
|
|
18
|
+
"podman"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"bun": ">=1.3.14"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.ts"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@atomservice/core": "0.1.5"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { tmpl } from "@atomservice/core"
|
|
2
|
+
import { REDIS_CONF_PATH, REDIS_IMAGE } from "./redis.consts.ts"
|
|
3
|
+
import type { RedisOptions } from "./redis.options.ts"
|
|
4
|
+
|
|
5
|
+
export function redisCompose(
|
|
6
|
+
containerName: string,
|
|
7
|
+
version: string,
|
|
8
|
+
dataDir: string,
|
|
9
|
+
confPath: string,
|
|
10
|
+
opts: Pick<RedisOptions, "password" | "hostPort">,
|
|
11
|
+
): string {
|
|
12
|
+
const { password, hostPort } = opts
|
|
13
|
+
|
|
14
|
+
const portsBlock = hostPort ? `\n ports:\n - "${hostPort}:6379"` : ""
|
|
15
|
+
const authEnv = password ? `\n environment:\n - REDISCLI_AUTH=${password}` : ""
|
|
16
|
+
|
|
17
|
+
return tmpl`\
|
|
18
|
+
services:
|
|
19
|
+
${containerName}:
|
|
20
|
+
image: ${REDIS_IMAGE}:${version}-alpine
|
|
21
|
+
container_name: ${containerName}
|
|
22
|
+
command: redis-server ${REDIS_CONF_PATH}${authEnv}
|
|
23
|
+
volumes:
|
|
24
|
+
- ${dataDir}:/data
|
|
25
|
+
- ${confPath}:${REDIS_CONF_PATH}:ro${portsBlock}
|
|
26
|
+
networks:
|
|
27
|
+
- atomservice
|
|
28
|
+
restart: unless-stopped
|
|
29
|
+
healthcheck:
|
|
30
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
31
|
+
interval: 10s
|
|
32
|
+
timeout: 5s
|
|
33
|
+
retries: 5
|
|
34
|
+
|
|
35
|
+
networks:
|
|
36
|
+
atomservice:
|
|
37
|
+
name: atomservice
|
|
38
|
+
external: true
|
|
39
|
+
`
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface RedisInstance {
|
|
2
|
+
/**
|
|
3
|
+
* 容器 `hostname`
|
|
4
|
+
*/
|
|
5
|
+
readonly host: string
|
|
6
|
+
/**
|
|
7
|
+
* 容器内端口
|
|
8
|
+
*/
|
|
9
|
+
readonly port: 6379
|
|
10
|
+
/**
|
|
11
|
+
* 访问密码
|
|
12
|
+
*
|
|
13
|
+
* - 未设置密码时为 `undefined`
|
|
14
|
+
*/
|
|
15
|
+
readonly password: string | undefined
|
|
16
|
+
/**
|
|
17
|
+
* 获取连接字符串
|
|
18
|
+
*
|
|
19
|
+
* @param db Redis 逻辑库编号(0-15),默认 0
|
|
20
|
+
*
|
|
21
|
+
* @example 有密码 `redis().connectionString()` → `redis://:password@redis:6379/0`
|
|
22
|
+
* @example 无密码 `redis().connectionString(1)` → `redis://redis:6379/1`
|
|
23
|
+
*/
|
|
24
|
+
connectionString(db?: number): string
|
|
25
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface RedisOptions {
|
|
2
|
+
/**
|
|
3
|
+
* 实例标识
|
|
4
|
+
*
|
|
5
|
+
* - 用于区分不同实例
|
|
6
|
+
*
|
|
7
|
+
* @default "default"
|
|
8
|
+
*/
|
|
9
|
+
id?: string
|
|
10
|
+
/**
|
|
11
|
+
* 镜像版本
|
|
12
|
+
*
|
|
13
|
+
* @default "8"
|
|
14
|
+
*/
|
|
15
|
+
version?: string
|
|
16
|
+
/**
|
|
17
|
+
* 访问密码
|
|
18
|
+
*
|
|
19
|
+
* - 不填则不设置密码(仅限容器内网访问时可不设)
|
|
20
|
+
*/
|
|
21
|
+
password?: string
|
|
22
|
+
/**
|
|
23
|
+
* 映射到宿主机的端口
|
|
24
|
+
*
|
|
25
|
+
* - 不填则不对外暴露,仅容器网络内访问
|
|
26
|
+
*/
|
|
27
|
+
hostPort?: number
|
|
28
|
+
/**
|
|
29
|
+
* 内存上限
|
|
30
|
+
*
|
|
31
|
+
* - 建议生产环境必设置,防止 `Redis` 吃满宿主机内存导致 `OOM`
|
|
32
|
+
* - 支持 Redis 原生格式:`"256mb"` / `"1gb"` 等
|
|
33
|
+
* - 不设置则无限制
|
|
34
|
+
*/
|
|
35
|
+
maxMemory?: string
|
|
36
|
+
/**
|
|
37
|
+
* 内存淘汰策略
|
|
38
|
+
*
|
|
39
|
+
* - 仅在设置了 `maxMemory` 时生效
|
|
40
|
+
* - `allkeys-lru`:淘汰最久未使用的 `key`(适合纯缓存场景)
|
|
41
|
+
* - `volatile-lru`:只淘汰设置了过期时间的 `key`
|
|
42
|
+
* - `noeviction`:不淘汰,写满后报错(适合 `Session` / 队列等不能丢数据的场景)
|
|
43
|
+
*
|
|
44
|
+
* @default "allkeys-lru"
|
|
45
|
+
*/
|
|
46
|
+
maxMemoryPolicy?: "allkeys-lru" | "allkeys-lfu" | "volatile-lru" | "volatile-lfu" | "volatile-ttl" | "noeviction"
|
|
47
|
+
/**
|
|
48
|
+
* 持久化方式
|
|
49
|
+
*
|
|
50
|
+
* - `"aof"`:`Append-Only File`,每次写操作追加日志,数据最安全(默认)
|
|
51
|
+
* - `"rdb"`:周期性快照,性能好但可能丢失最近数据
|
|
52
|
+
* - `"both"`:同时开启 `AOF` 和 `RDB`,兼顾性能和安全
|
|
53
|
+
* - `"none"`:不持久化,重启数据全部丢失(纯缓存场景可用)
|
|
54
|
+
*
|
|
55
|
+
* @default "aof"
|
|
56
|
+
*/
|
|
57
|
+
persistence?: "aof" | "rdb" | "both" | "none"
|
|
58
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import type { CallableService } from "@atomservice/core"
|
|
3
|
+
import { defineService, onDown, onHealth, onUp, useConfig, useLogger, useShell, useState } from "@atomservice/core"
|
|
4
|
+
import { redisCompose } from "./redis.compose.ts"
|
|
5
|
+
import type { RedisInstance } from "./redis.instance.ts"
|
|
6
|
+
import type { RedisOptions } from "./redis.options.ts"
|
|
7
|
+
import type { RedisState } from "./redis.types.ts"
|
|
8
|
+
import { buildRedisConf } from "./redis.utils.ts"
|
|
9
|
+
|
|
10
|
+
export function redis(opts: RedisOptions = {}): CallableService<RedisInstance> {
|
|
11
|
+
const serviceId = opts.id && opts.id !== "default" ? opts.id : "default"
|
|
12
|
+
const containerName = serviceId !== "default" ? `redis-${serviceId}` : "redis"
|
|
13
|
+
const version = opts.version ?? "8"
|
|
14
|
+
|
|
15
|
+
return defineService({
|
|
16
|
+
name: "redis",
|
|
17
|
+
id: opts.id,
|
|
18
|
+
setup: () => {
|
|
19
|
+
const $ = useShell()
|
|
20
|
+
const config = useConfig()
|
|
21
|
+
const logger = useLogger()
|
|
22
|
+
|
|
23
|
+
const dir = path.join(config.root, containerName)
|
|
24
|
+
const composePath = path.join(dir, "compose.yml")
|
|
25
|
+
const confPath = path.join(dir, "redis.conf")
|
|
26
|
+
|
|
27
|
+
const { save, watch, checkChanges } = useState<RedisState>()
|
|
28
|
+
|
|
29
|
+
watch("version", {
|
|
30
|
+
risk: "low",
|
|
31
|
+
changed: (previousValue, currentValue) =>
|
|
32
|
+
`Redis 版本已变更 ${previousValue} → ${currentValue},将在下次启动时拉取新镜像`,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
watch("password", {
|
|
36
|
+
risk: "high",
|
|
37
|
+
added: () => `已新增密码认证,需重启容器并更新所有客户端连接字符串`,
|
|
38
|
+
removed: () => `已移除密码认证,需重启容器并更新所有客户端连接字符串`,
|
|
39
|
+
changed: () => `密码已变更,需重启容器并更新所有客户端连接字符串`,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
onUp(async () => {
|
|
43
|
+
const currentState: RedisState = { version, password: opts.password }
|
|
44
|
+
await checkChanges(currentState)
|
|
45
|
+
|
|
46
|
+
await $`mkdir -p ${dir}/data`
|
|
47
|
+
await Bun.write(confPath, buildRedisConf(opts))
|
|
48
|
+
await Bun.write(composePath, redisCompose(containerName, version, `${dir}/data`, confPath, opts))
|
|
49
|
+
|
|
50
|
+
logger.info(`正在启动 ${containerName}`)
|
|
51
|
+
await $`podman compose -f ${composePath} up -d`
|
|
52
|
+
logger.success(`${containerName} 已启动`)
|
|
53
|
+
|
|
54
|
+
await save(currentState)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
onDown(async () => {
|
|
58
|
+
logger.info(`停止 ${containerName}…`)
|
|
59
|
+
await $`podman compose -f ${composePath} down`
|
|
60
|
+
logger.success(`${containerName} 已停止`)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
onHealth(async () => {
|
|
64
|
+
try {
|
|
65
|
+
const result = opts.password
|
|
66
|
+
? await $`podman exec -e ${`REDISCLI_AUTH=${opts.password}`} ${containerName} redis-cli ping`.text()
|
|
67
|
+
: await $`podman exec ${containerName} redis-cli ping`.text()
|
|
68
|
+
if (result.trim() === "PONG") return { status: "healthy" }
|
|
69
|
+
return {
|
|
70
|
+
status: "unhealthy",
|
|
71
|
+
message: `redis-cli ping 返回:${result.trim()}`,
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
return {
|
|
75
|
+
status: "unhealthy",
|
|
76
|
+
message: `容器 ${containerName} 不存在`,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
get host() {
|
|
83
|
+
return containerName
|
|
84
|
+
},
|
|
85
|
+
get port(): 6379 {
|
|
86
|
+
return 6379
|
|
87
|
+
},
|
|
88
|
+
get password() {
|
|
89
|
+
return opts.password
|
|
90
|
+
},
|
|
91
|
+
connectionString(db = 0): string {
|
|
92
|
+
const base = opts.password
|
|
93
|
+
? `redis://:${opts.password}@${containerName}:6379`
|
|
94
|
+
: `redis://${containerName}:6379`
|
|
95
|
+
return `${base}/${db}`
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RedisOptions } from "./redis.options.ts"
|
|
2
|
+
|
|
3
|
+
export function buildRedisConf(
|
|
4
|
+
opts: Pick<RedisOptions, "password" | "maxMemory" | "maxMemoryPolicy" | "persistence">,
|
|
5
|
+
): string {
|
|
6
|
+
const { password, maxMemory, maxMemoryPolicy = "allkeys-lru", persistence = "aof" } = opts
|
|
7
|
+
const lines: string[] = []
|
|
8
|
+
if (password) lines.push(`requirepass ${password}`)
|
|
9
|
+
if (persistence === "aof" || persistence === "both") lines.push("appendonly yes")
|
|
10
|
+
if (persistence === "rdb" || persistence === "both") lines.push("save 3600 1 300 100 60 10000")
|
|
11
|
+
if (maxMemory) {
|
|
12
|
+
lines.push(`maxmemory ${maxMemory}`)
|
|
13
|
+
lines.push(`maxmemory-policy ${maxMemoryPolicy}`)
|
|
14
|
+
}
|
|
15
|
+
return lines.length ? `${lines.join("\n")}\n` : ""
|
|
16
|
+
}
|