Stabilize docker service startup
This commit is contained in:
@@ -1,17 +1,19 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"errors"
|
"strconv"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `yaml:"server"`
|
Server ServerConfig `yaml:"server"`
|
||||||
Database DatabaseConfig `yaml:"database"`
|
Database DatabaseConfig `yaml:"database"`
|
||||||
Redis RedisConfig `yaml:"redis"`
|
Redis RedisConfig `yaml:"redis"`
|
||||||
RabbitMQ RabbitMQConfig `yaml:"rabbitmq"`
|
RabbitMQ RabbitMQConfig `yaml:"rabbitmq"`
|
||||||
ObservabilityConfig ObservabilityConfig `yaml:"observability"`
|
ObservabilityConfig ObservabilityConfig `yaml:"observability"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,10 +47,11 @@ type ObservabilityConfig struct {
|
|||||||
Pprof PprofConfig `yaml:"pprof"`
|
Pprof PprofConfig `yaml:"pprof"`
|
||||||
}
|
}
|
||||||
type PprofConfig struct {
|
type PprofConfig struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
ApiAddr string `yaml:"api_addr"`
|
ApiAddr string `yaml:"api_addr"`
|
||||||
WorkerAddr string `yaml:"worker_addr"`
|
WorkerAddr string `yaml:"worker_addr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(filename string) (Config, error) {
|
func Load(filename string) (Config, error) {
|
||||||
data, err := os.ReadFile(filename)
|
data, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -60,9 +63,71 @@ func Load(filename string) (Config, error) {
|
|||||||
return Config{}, fmt.Errorf("parse config %s: %w", filename, err)
|
return Config{}, fmt.Errorf("parse config %s: %w", filename, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplyEnvOverrides(&cfg)
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ApplyEnvOverrides(cfg *Config) {
|
||||||
|
if cfg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v := os.Getenv("SERVER_PORT"); v != "" {
|
||||||
|
if port, err := strconv.Atoi(v); err == nil {
|
||||||
|
cfg.Server.Port = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := os.Getenv("MYSQL_HOST"); v != "" {
|
||||||
|
cfg.Database.Host = v
|
||||||
|
}
|
||||||
|
if v := os.Getenv("MYSQL_PORT"); v != "" {
|
||||||
|
if port, err := strconv.Atoi(v); err == nil {
|
||||||
|
cfg.Database.Port = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := os.Getenv("MYSQL_USER"); v != "" {
|
||||||
|
cfg.Database.User = v
|
||||||
|
}
|
||||||
|
if v := os.Getenv("MYSQL_ROOT_PASSWORD"); v != "" {
|
||||||
|
cfg.Database.Password = v
|
||||||
|
}
|
||||||
|
if v := os.Getenv("MYSQL_PASSWORD"); v != "" {
|
||||||
|
cfg.Database.Password = v
|
||||||
|
}
|
||||||
|
if v := os.Getenv("MYSQL_DATABASE"); v != "" {
|
||||||
|
cfg.Database.DBName = v
|
||||||
|
}
|
||||||
|
if v := os.Getenv("REDIS_HOST"); v != "" {
|
||||||
|
cfg.Redis.Host = v
|
||||||
|
}
|
||||||
|
if v := os.Getenv("REDIS_PORT"); v != "" {
|
||||||
|
if port, err := strconv.Atoi(v); err == nil {
|
||||||
|
cfg.Redis.Port = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := os.Getenv("REDIS_PASSWORD"); v != "" {
|
||||||
|
cfg.Redis.Password = v
|
||||||
|
}
|
||||||
|
if v := os.Getenv("REDIS_DB"); v != "" {
|
||||||
|
if db, err := strconv.Atoi(v); err == nil {
|
||||||
|
cfg.Redis.DB = db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := os.Getenv("RABBITMQ_HOST"); v != "" {
|
||||||
|
cfg.RabbitMQ.Host = v
|
||||||
|
}
|
||||||
|
if v := os.Getenv("RABBITMQ_PORT"); v != "" {
|
||||||
|
if port, err := strconv.Atoi(v); err == nil {
|
||||||
|
cfg.RabbitMQ.Port = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v := os.Getenv("RABBITMQ_USER"); v != "" {
|
||||||
|
cfg.RabbitMQ.Username = v
|
||||||
|
}
|
||||||
|
if v := os.Getenv("RABBITMQ_PASS"); v != "" {
|
||||||
|
cfg.RabbitMQ.Password = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// bool用来表示是否使用了默认配置,true表示使用了默认配置
|
// bool用来表示是否使用了默认配置,true表示使用了默认配置
|
||||||
func LoadLocalDev(filename string) (Config, bool, error) {
|
func LoadLocalDev(filename string) (Config, bool, error) {
|
||||||
cfg, err := Load(filename)
|
cfg, err := Load(filename)
|
||||||
@@ -76,14 +141,14 @@ func LoadLocalDev(filename string) (Config, bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DefaultLocalConfig() Config {
|
func DefaultLocalConfig() Config {
|
||||||
return Config{
|
cfg := Config{
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
},
|
},
|
||||||
Database: DatabaseConfig{
|
Database: DatabaseConfig{
|
||||||
Host: "localhost",
|
Host: "localhost",
|
||||||
Port: 3306,
|
Port: 3306,
|
||||||
User: "root",
|
User: "root",
|
||||||
Password: "123456",
|
Password: "123456",
|
||||||
DBName: "feedsystem",
|
DBName: "feedsystem",
|
||||||
},
|
},
|
||||||
@@ -101,10 +166,12 @@ func DefaultLocalConfig() Config {
|
|||||||
},
|
},
|
||||||
ObservabilityConfig: ObservabilityConfig{
|
ObservabilityConfig: ObservabilityConfig{
|
||||||
Pprof: PprofConfig{
|
Pprof: PprofConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
ApiAddr: "localhost:6060",
|
ApiAddr: "localhost:6060",
|
||||||
WorkerAddr: "localhost:6061",
|
WorkerAddr: "localhost:6061",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
ApplyEnvOverrides(&cfg)
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,93 +1,107 @@
|
|||||||
package worker
|
package worker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"feedsystem_video_go/internal/middleware/rabbitmq"
|
"feedsystem_video_go/internal/middleware/rabbitmq"
|
||||||
"feedsystem_video_go/internal/middleware/redis"
|
"feedsystem_video_go/internal/middleware/redis"
|
||||||
"feedsystem_video_go/internal/video"
|
"feedsystem_video_go/internal/video"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
oredis "github.com/redis/go-redis/v9"
|
oredis "github.com/redis/go-redis/v9"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartOutboxPoller(db *gorm.DB, tmq *rabbitmq.TimelineMQ) {
|
func StartOutboxPoller(db *gorm.DB, tmq *rabbitmq.TimelineMQ) {
|
||||||
go func() {
|
if db == nil || tmq == nil || tmq.RabbitMQ == nil || tmq.Ch == nil {
|
||||||
for {
|
log.Printf("Outbox poller disabled: timeline mq is not initialized")
|
||||||
var messages []video.OutboxMsg
|
return
|
||||||
|
}
|
||||||
err := db.Where("status = ?", "pending").Order("create_time ASC").Limit(100).Find(&messages).Error
|
|
||||||
|
go func() {
|
||||||
if err != nil || len(messages) == 0 {
|
for {
|
||||||
time.Sleep(1 * time.Second)
|
var messages []video.OutboxMsg
|
||||||
continue
|
|
||||||
}
|
err := db.Where("status = ?", "pending").Order("create_time ASC").Limit(100).Find(&messages).Error
|
||||||
|
|
||||||
for _, msg := range messages {
|
if err != nil || len(messages) == 0 {
|
||||||
err := tmq.PublishVideo(context.Background(), msg.VideoID, msg.CreateTime)
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
if err == nil {
|
}
|
||||||
db.Delete(&msg)
|
|
||||||
} else {
|
for _, msg := range messages {
|
||||||
log.Printf("投递MQ失败: VideoID: %d, err: %v", msg.VideoID, err)
|
err := tmq.PublishVideo(context.Background(), msg.VideoID, msg.CreateTime)
|
||||||
}
|
|
||||||
}
|
if err == nil {
|
||||||
}
|
db.Delete(&msg)
|
||||||
}()
|
} else {
|
||||||
}
|
log.Printf("投递MQ失败: VideoID: %d, err: %v", msg.VideoID, err)
|
||||||
|
}
|
||||||
func StartConsumer(tmq *rabbitmq.TimelineMQ, queueName string, redisClient *redis.Client) {
|
}
|
||||||
msgs, err := tmq.Ch.Consume(
|
}
|
||||||
queueName,
|
}()
|
||||||
"",
|
}
|
||||||
false,
|
|
||||||
false,
|
func StartConsumer(tmq *rabbitmq.TimelineMQ, queueName string, redisClient *redis.Client) {
|
||||||
false,
|
if tmq == nil || tmq.RabbitMQ == nil || tmq.Ch == nil {
|
||||||
false,
|
log.Printf("Timeline consumer disabled: timeline mq is not initialized")
|
||||||
nil,
|
return
|
||||||
)
|
}
|
||||||
|
if redisClient == nil {
|
||||||
if err != nil {
|
log.Printf("Timeline consumer disabled: redis is not initialized")
|
||||||
log.Printf("注册消费失败")
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
msgs, err := tmq.Ch.Consume(
|
||||||
go func() {
|
queueName,
|
||||||
for msg := range msgs {
|
"",
|
||||||
var event rabbitmq.TimelineEvent
|
false,
|
||||||
err := json.Unmarshal(msg.Body, &event)
|
false,
|
||||||
|
false,
|
||||||
if err != nil {
|
false,
|
||||||
log.Printf("反序列化失败")
|
nil,
|
||||||
msg.Ack(false)
|
)
|
||||||
continue
|
|
||||||
}
|
if err != nil {
|
||||||
|
log.Printf("注册消费失败")
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
return
|
||||||
timelineKey := redisClient.Key("feed:global_timeline")
|
}
|
||||||
err = redisClient.ZAdd(ctx, timelineKey, oredis.Z{
|
|
||||||
Score: float64(event.CreateTime),
|
go func() {
|
||||||
Member: fmt.Sprintf("%d", event.VideoID),
|
for msg := range msgs {
|
||||||
})
|
var event rabbitmq.TimelineEvent
|
||||||
|
err := json.Unmarshal(msg.Body, &event)
|
||||||
if err != nil {
|
|
||||||
log.Printf("写入Zset失败")
|
if err != nil {
|
||||||
msg.Nack(false, true)
|
log.Printf("反序列化失败")
|
||||||
cancel()
|
msg.Ack(false)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = redisClient.ZRemRangeByRank(ctx, timelineKey, 0, -1001)
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||||
|
timelineKey := redisClient.Key("feed:global_timeline")
|
||||||
if err != nil {
|
err = redisClient.ZAdd(ctx, timelineKey, oredis.Z{
|
||||||
log.Printf("ZRem失败")
|
Score: float64(event.CreateTime),
|
||||||
}
|
Member: fmt.Sprintf("%d", event.VideoID),
|
||||||
|
})
|
||||||
msg.Ack(false)
|
|
||||||
cancel()
|
if err != nil {
|
||||||
}
|
log.Printf("写入Zset失败")
|
||||||
}()
|
msg.Nack(false, true)
|
||||||
}
|
cancel()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = redisClient.ZRemRangeByRank(ctx, timelineKey, 0, -1001)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ZRem失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Ack(false)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,22 +16,24 @@ services:
|
|||||||
- --default-authentication-plugin=mysql_native_password
|
- --default-authentication-plugin=mysql_native_password
|
||||||
- --character-set-server=utf8mb4
|
- --character-set-server=utf8mb4
|
||||||
- --collation-server=utf8mb4_0900_ai_ci
|
- --collation-server=utf8mb4_0900_ai_ci
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p123456 --silent"]
|
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$${MYSQL_ROOT_PASSWORD} --silent"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: always
|
restart: always
|
||||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-123456}"]
|
environment:
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:-123456}
|
||||||
|
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-123456}"]
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "-a", "123456", "ping"]
|
test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" ping"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 20
|
retries: 20
|
||||||
@@ -59,8 +61,13 @@ services:
|
|||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
target: api
|
target: api
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
JWT_SECRET: ${JWT_SECRET:-feedsystem-dev-secret-key}
|
JWT_SECRET: ${JWT_SECRET:-feedsystem-dev-secret-key}
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-feedsystem}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:-123456}
|
||||||
|
RABBITMQ_USER: ${RABBITMQ_USER:-admin}
|
||||||
|
RABBITMQ_PASS: ${RABBITMQ_PASS:-password123}
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -85,8 +92,13 @@ services:
|
|||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
target: worker
|
target: worker
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
JWT_SECRET: ${JWT_SECRET:-feedsystem-dev-secret-key}
|
JWT_SECRET: ${JWT_SECRET:-feedsystem-dev-secret-key}
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-feedsystem}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:-123456}
|
||||||
|
RABBITMQ_USER: ${RABBITMQ_USER:-admin}
|
||||||
|
RABBITMQ_PASS: ${RABBITMQ_PASS:-password123}
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/configs/config.docker.yaml:/app/configs/config.yaml:ro
|
- ./backend/configs/config.docker.yaml:/app/configs/config.yaml:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
Reference in New Issue
Block a user