feat: implement WebServer, Reloader interface and atomic hot-reload methods ReplaceProxies, ReplaceRewrites, ReplaceStatics (by AI)
This commit is contained in:
parent
c207bdd400
commit
3925767d2e
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,9 +1,8 @@
|
||||
.log.meta.json
|
||||
|
||||
.ai/
|
||||
|
||||
.geminiignore
|
||||
.gemini
|
||||
env.json
|
||||
env.yml
|
||||
env.yaml
|
||||
/CODE-FULL.md
|
||||
|
||||
3
go.mod
3
go.mod
@ -11,6 +11,7 @@ require (
|
||||
apigo.cc/go/log v1.3.0
|
||||
apigo.cc/go/redis v1.3.0
|
||||
apigo.cc/go/safe v1.3.0
|
||||
apigo.cc/go/starter v1.0.1
|
||||
apigo.cc/go/timer v1.3.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
golang.org/x/net v0.54.0
|
||||
@ -22,7 +23,7 @@ require (
|
||||
apigo.cc/go/encoding v1.3.0 // indirect
|
||||
apigo.cc/go/rand v1.3.0 // indirect
|
||||
apigo.cc/go/shell v1.3.0 // indirect
|
||||
github.com/gomodule/redigo v1.9.3 // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
|
||||
68
go.sum
68
go.sum
@ -1,47 +1,43 @@
|
||||
apigo.cc/go/cast v1.2.10 h1:wa9/hz6GW6Z+5co6l7LftMn2Eo06WpVHHDCCQphnmH8=
|
||||
apigo.cc/go/cast v1.2.10/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||
apigo.cc/go/config v1.0.8 h1:ZvontnJngNJrm6EJAPYmPhmBnLC9V7g5kZLiuN1MT60=
|
||||
apigo.cc/go/config v1.0.8/go.mod h1:FCZj70MCejeWwv81O7sdpg0zmjOzglAMmNEfT3dQYzw=
|
||||
apigo.cc/go/crypto v1.1.1 h1:AE0jNtKzcq4euz6fL9MAYEHQpbIEfDTHv2mriP/juig=
|
||||
apigo.cc/go/crypto v1.1.1/go.mod h1:Q26As+TQrNs6olGkiVdD6649DJirxA4CUBT4oukKPuw=
|
||||
apigo.cc/go/discover v1.0.11 h1:aeAC+xAwGlOeXsRptXJkEn8MvRZ7lom5N5jfBAg9/CE=
|
||||
apigo.cc/go/discover v1.0.11/go.mod h1:TcIpl1Ocu51koRxugV81Jnz4NH0+Q5f5PF105VczS/0=
|
||||
apigo.cc/go/encoding v1.1.2 h1:reSrLkyYrtZsf4S91XPdyBY2AQpvA43n9q0Q9wz5uJA=
|
||||
apigo.cc/go/encoding v1.1.2/go.mod h1:iLuvrYHEK8mLnk8jijx5Sv1tInFreny0yGNBouA1d20=
|
||||
apigo.cc/go/file v1.0.8 h1:GPkixU080cvrmz7cbdXkC2DqMvsWWyY3UzoyUVQYFvs=
|
||||
apigo.cc/go/file v1.0.8/go.mod h1:T/wYji/va0S+JM2fAHonhKpnXKIELk/bmgnFEgMMY2s=
|
||||
apigo.cc/go/http v1.0.11 h1:EOlMXlTGrWY0RI3MynkV7noT49WiUdGVPdOtDJjIkU4=
|
||||
apigo.cc/go/http v1.0.11/go.mod h1:K2JgyI7DblfbzAnK1OHx4PS/1Pvcoqcp3g2uwsCPe68=
|
||||
apigo.cc/go/id v1.0.7 h1:vXCK8mUW3s4cJYmli0o2BxgyI9XbJrG8gSGJOP2Fe4g=
|
||||
apigo.cc/go/id v1.0.7/go.mod h1:wXBrPpcEpyUDM7bp7M5uPM9zFw4VcnvXMQLw4Yd+uZE=
|
||||
apigo.cc/go/log v1.1.16 h1:uqPqeHvs+FdNupLBzzamJmY4oHAqtPEkGuW/pW5i2nQ=
|
||||
apigo.cc/go/log v1.1.16/go.mod h1:bOfPXjrX2bY+FNG9eEtBnvaVXoxZDGvz0jQfF3s/mYk=
|
||||
apigo.cc/go/rand v1.0.6 h1:p51rkaDrYUdZPIRbQAujZmQelWg2ipAMts33A/tG7QE=
|
||||
apigo.cc/go/rand v1.0.6/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||
apigo.cc/go/redis v1.0.9 h1:5MnCchcDgoVnGHQn+KJF9PJXXRayK9nlZg+Q0lTnMoU=
|
||||
apigo.cc/go/redis v1.0.9/go.mod h1:SVOD7iuUL/jxYEa28qObDQf4GY9UVSKjhM9vVj9TXLI=
|
||||
apigo.cc/go/safe v1.0.7 h1:f0d+v9K2dHPyG5DNqhyddCmAmSiIqIfkPi/AMED/iQI=
|
||||
apigo.cc/go/safe v1.0.7/go.mod h1:Hu7TVDWPe/I+nBZfYJH4mt+ROzG+rwk2D1zHTXj/2eE=
|
||||
apigo.cc/go/shell v1.0.6 h1:RngaSMr2AkAFDl545A1Ln+D8ckqV2jknUp4PohDaLIA=
|
||||
apigo.cc/go/shell v1.0.6/go.mod h1:X7Nozjd7oau4nvAJCI21vxrxfd4ZL5nE4C6eUsmi2Hc=
|
||||
apigo.cc/go/timer v1.0.7 h1:QUH0t7l9kBiGU/QdDNSthnXLfJXOEp+mpdY2+QPlrEI=
|
||||
apigo.cc/go/timer v1.0.7/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
||||
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
apigo.cc/go/cast v1.3.0 h1:ZTcLYijkqZjSWSCSpJUWMfzJYeJKbwKxquKkPrFsROQ=
|
||||
apigo.cc/go/cast v1.3.0/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||
apigo.cc/go/config v1.3.0 h1:TwI3bv3D+BJrAnFx+o62HQo3FarY2Ge3SCGsKchFYGg=
|
||||
apigo.cc/go/config v1.3.0/go.mod h1:88lqKEBXlIExFKt1geLONVLYyM+QhRVpBe0ok3OEvjI=
|
||||
apigo.cc/go/crypto v1.3.0 h1:rGRrrb5O+4M50X5hVUmJQbXx3l87zzlcgzGtUvZrZL8=
|
||||
apigo.cc/go/crypto v1.3.0/go.mod h1:uSCcmbcFoiltUPMQTSuqmU9nfKEH/lRs7nQ7aa3Z4Mc=
|
||||
apigo.cc/go/discover v1.3.0 h1:CXuKtAZygU+4TMHtebVkjWyyWmPgoLbsJFdKFGiCOd8=
|
||||
apigo.cc/go/discover v1.3.0/go.mod h1:VMu1qC6AngVFQMdaCwGoq3/PPX0xDnjkG+1AcSA+Zvs=
|
||||
apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs=
|
||||
apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo=
|
||||
apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg=
|
||||
apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY=
|
||||
apigo.cc/go/http v1.3.0 h1:1ZweotOuAxTI8wfib9knWYXM2t0POOJ3ezgOKObH3sg=
|
||||
apigo.cc/go/http v1.3.0/go.mod h1:DC3phxBNbt/dOWdhxtffAEYeUs3j6P3BD8e6J8gxU9U=
|
||||
apigo.cc/go/id v1.3.0 h1:Tr2Yj0Rl19lfwW5wBTJ407o/zgo2oVRLE20WWEgJzdE=
|
||||
apigo.cc/go/id v1.3.0/go.mod h1:AFH3kMFwENfXNyijnAFWEhSF1o3y++UBPem1IUlrcxA=
|
||||
apigo.cc/go/log v1.3.0 h1:61Z80WGN6SnhgxgoR8xuVYIieMdjlJKmf8JX1HXzp0Y=
|
||||
apigo.cc/go/log v1.3.0/go.mod h1:dz4bSz9BnOgutkUJJZfX3uDDwsMpUxt7WF50mLK9hgE=
|
||||
apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU=
|
||||
apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||
apigo.cc/go/redis v1.3.0 h1:3NJE3xPXzhCwL+Mh1iyphFrsKWEuPlY26LHJfMVFSeU=
|
||||
apigo.cc/go/redis v1.3.0/go.mod h1:KPDPwMOER7WJX3Qev24LTeAOSmCl8OApe8iagPDxOUQ=
|
||||
apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok=
|
||||
apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs=
|
||||
apigo.cc/go/shell v1.3.0 h1:hdxuYPN/7T2BuM/Ja8AjVUhbRqU/wpi8OjcJVziJ0nw=
|
||||
apigo.cc/go/shell v1.3.0/go.mod h1:aNJiRWibxlA485yX3t+07IVAbrALKmxzv4oGEUC+hK4=
|
||||
apigo.cc/go/starter v1.0.1 h1:7Qv/rRlEVlTX7wjr1LpV1XX1wUD4UAssDi6J+YCh73s=
|
||||
apigo.cc/go/starter v1.0.1/go.mod h1:xHfo+36hXGdVhhnRqd1l+Vk6Fp1ecN2LDAcsDOVodXk=
|
||||
apigo.cc/go/timer v1.3.0 h1:dorVGKw0xR6Gj8Pwfl86K46szMBfD31XyO+uUqxU+EI=
|
||||
apigo.cc/go/timer v1.3.0/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
|
||||
28
proxy.go
28
proxy.go
@ -162,3 +162,31 @@ func copyResponse(res *gohttp.Result, response *Response, logger *log.Logger) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyRule 定义了外部传递或 Redis 中获取的代理规则配置
|
||||
type ProxyRule struct {
|
||||
Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$
|
||||
AuthLevel int // 所需鉴权级别
|
||||
ToApp string // 目标 AppName 或完整 URL (可含 $1 变量替换)
|
||||
ToPath string // 目标路径 (可含 $1 变量替换)
|
||||
}
|
||||
|
||||
// ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的所有代理规则。
|
||||
// 该方法非常轻量,仅在赋值瞬间短暂持有写锁,不会阻塞任何并发请求,并且自动淘汰旧规则。
|
||||
func ReplaceProxies(host string, rules []ProxyRule) {
|
||||
newProxies := make([]*proxyType, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
p := &proxyType{authLevel: r.AuthLevel, fromPath: r.Path, toApp: r.ToApp, toPath: r.ToPath}
|
||||
if strings.ContainsRune(r.Path, '(') {
|
||||
matcher, err := regexp.Compile("^" + r.Path + "$")
|
||||
if err == nil {
|
||||
p.matcher = matcher
|
||||
}
|
||||
}
|
||||
newProxies = append(newProxies, p)
|
||||
}
|
||||
|
||||
hostPoliciesLock.Lock()
|
||||
defer hostPoliciesLock.Unlock()
|
||||
hostProxies[host] = newProxies
|
||||
}
|
||||
|
||||
33
reload.go
Normal file
33
reload.go
Normal file
@ -0,0 +1,33 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apigo.cc/go/log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
reloadHooks []func() error
|
||||
reloadLock sync.RWMutex
|
||||
)
|
||||
|
||||
// OnReload 注册一个在接收到 SIGHUP 信号时触发的重新加载钩子
|
||||
func OnReload(handler func() error) {
|
||||
reloadLock.Lock()
|
||||
defer reloadLock.Unlock()
|
||||
reloadHooks = append(reloadHooks, handler)
|
||||
}
|
||||
|
||||
// triggerReload 触发所有注册的重新加载钩子
|
||||
func triggerReload() error {
|
||||
reloadLock.RLock()
|
||||
hooks := make([]func() error, len(reloadHooks))
|
||||
copy(hooks, reloadHooks)
|
||||
reloadLock.RUnlock()
|
||||
|
||||
for _, hook := range hooks {
|
||||
if err := hook(); err != nil {
|
||||
log.DefaultLogger.Error("reload hook failed", "error", err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
25
rewrite.go
25
rewrite.go
@ -101,3 +101,28 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// RewriteRule 定义了外部传递的 URL 重写规则
|
||||
type RewriteRule struct {
|
||||
Path string // 原始路径或匹配正则,例如 ^/old/(.*)$
|
||||
ToPath string // 重写后的路径,例如 /new/$1
|
||||
}
|
||||
|
||||
// ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的所有重写规则。
|
||||
func ReplaceRewrites(host string, rules []RewriteRule) {
|
||||
newRewrites := make([]*rewriteType, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
s := &rewriteType{fromPath: r.Path, toPath: r.ToPath}
|
||||
if strings.ContainsRune(r.Path, '(') {
|
||||
matcher, err := regexp.Compile("^" + r.Path + "$")
|
||||
if err == nil {
|
||||
s.matcher = matcher
|
||||
}
|
||||
}
|
||||
newRewrites = append(newRewrites, s)
|
||||
}
|
||||
|
||||
hostPoliciesLock.Lock()
|
||||
defer hostPoliciesLock.Unlock()
|
||||
hostRewrites[host] = newRewrites
|
||||
}
|
||||
|
||||
144
server.go
144
server.go
@ -5,6 +5,7 @@ import (
|
||||
"apigo.cc/go/log"
|
||||
"apigo.cc/go/redis"
|
||||
"apigo.cc/go/safe"
|
||||
"apigo.cc/go/starter"
|
||||
"context"
|
||||
"fmt"
|
||||
"golang.org/x/net/http2"
|
||||
@ -12,46 +13,35 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GlobalDiscoverer 供服务框架内部使用的发现实例
|
||||
var GlobalDiscoverer *discover.Discoverer
|
||||
|
||||
// AsyncServer 异步服务实例
|
||||
type AsyncServer struct {
|
||||
// WebServer 实现了 starter.Service 和 starter.Reloader 接口
|
||||
type WebServer struct {
|
||||
server *http.Server
|
||||
listener net.Listener
|
||||
Addr string
|
||||
stopChan chan os.Signal
|
||||
startChan chan bool
|
||||
useDiscover bool
|
||||
discoverer *discover.Discoverer
|
||||
}
|
||||
|
||||
// AsyncStart 异步启动服务
|
||||
func AsyncStart() *AsyncServer {
|
||||
as := &AsyncServer{
|
||||
startChan: make(chan bool, 1),
|
||||
stopChan: make(chan os.Signal, 1),
|
||||
// NewWebServer 创建并返回一个新的 WebServer 实例
|
||||
func NewWebServer() *WebServer {
|
||||
return &WebServer{}
|
||||
}
|
||||
|
||||
go as.start()
|
||||
|
||||
<-as.startChan
|
||||
return as
|
||||
}
|
||||
|
||||
func (as *AsyncServer) start() {
|
||||
// Start 启动服务,实现 starter.Service 接口
|
||||
func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error {
|
||||
listenStr := Config.Listen
|
||||
as.useDiscover = false
|
||||
ws.useDiscover = false
|
||||
|
||||
if listenStr == "" {
|
||||
listenStr = ":0,h2c"
|
||||
as.useDiscover = true
|
||||
ws.useDiscover = true
|
||||
}
|
||||
|
||||
// 解析第一个监听配置
|
||||
@ -76,7 +66,7 @@ func (as *AsyncServer) start() {
|
||||
}
|
||||
appName := Config.App
|
||||
if appName != "" || Config.Register != "" {
|
||||
as.useDiscover = true
|
||||
ws.useDiscover = true
|
||||
}
|
||||
|
||||
// 初始化服务器唯一标识 (8位,物理上限 3,844/s)
|
||||
@ -92,18 +82,16 @@ func (as *AsyncServer) start() {
|
||||
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.DefaultLogger.Error("failed to listen", "addr", addr, "error", err.Error())
|
||||
as.startChan <- false
|
||||
return
|
||||
return fmt.Errorf("failed to listen on %s: %w", addr, err)
|
||||
}
|
||||
|
||||
as.listener = listener
|
||||
as.Addr = listener.Addr().String()
|
||||
serverAddr = as.Addr
|
||||
ws.listener = listener
|
||||
ws.Addr = listener.Addr().String()
|
||||
serverAddr = ws.Addr
|
||||
|
||||
// 如果使用了随机端口且没有明确指定不需要服务发现,则开启
|
||||
if addr == ":0" || strings.HasSuffix(addr, ":0") {
|
||||
as.useDiscover = true
|
||||
ws.useDiscover = true
|
||||
}
|
||||
|
||||
h2s := &http2.Server{}
|
||||
@ -112,7 +100,7 @@ func (as *AsyncServer) start() {
|
||||
handler = h2c.NewHandler(handler, h2s)
|
||||
}
|
||||
|
||||
as.server = &http.Server{
|
||||
ws.server = &http.Server{
|
||||
Handler: handler,
|
||||
ReadTimeout: time.Duration(Config.ReadTimeout) * time.Millisecond,
|
||||
ReadHeaderTimeout: time.Duration(Config.ReadHeaderTimeout) * time.Millisecond,
|
||||
@ -122,8 +110,8 @@ func (as *AsyncServer) start() {
|
||||
}
|
||||
|
||||
// 启动服务发现
|
||||
if as.useDiscover {
|
||||
_, port, _ := net.SplitHostPort(as.Addr)
|
||||
if ws.useDiscover {
|
||||
_, port, _ := net.SplitHostPort(ws.Addr)
|
||||
ip := GetServerIp()
|
||||
discoverAddr := fmt.Sprintf("%s:%s", ip, port)
|
||||
|
||||
@ -163,52 +151,98 @@ func (as *AsyncServer) start() {
|
||||
registry = "127.0.0.1:6379::15" // Default fallback
|
||||
}
|
||||
|
||||
as.discoverer = discover.Start(registry, appName, discoverAddr, log.DefaultLogger, discConf)
|
||||
GlobalDiscoverer = as.discoverer
|
||||
if as.discoverer != nil {
|
||||
log.DefaultLogger.Info("discover registered", "app", appName, "addr", discoverAddr)
|
||||
ws.discoverer = discover.Start(registry, appName, discoverAddr, logger, discConf)
|
||||
GlobalDiscoverer = ws.discoverer
|
||||
if ws.discoverer != nil {
|
||||
logger.Info("discover registered", "app", appName, "addr", discoverAddr)
|
||||
}
|
||||
}
|
||||
|
||||
signal.Notify(as.stopChan, os.Interrupt, syscall.SIGTERM)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
log.DefaultLogger.Info("service starting", "addr", as.Addr, "proto", protocol)
|
||||
as.startChan <- true
|
||||
if err := as.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
log.DefaultLogger.Error("server error", "error", err.Error())
|
||||
logger.Info("service starting", "addr", ws.Addr, "proto", protocol)
|
||||
if err := ws.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
errChan <- err
|
||||
}
|
||||
close(errChan)
|
||||
}()
|
||||
|
||||
// 短暂等待验证是否闪退
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (as *AsyncServer) Stop() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止服务,实现 starter.Service 接口
|
||||
func (ws *WebServer) Stop(ctx context.Context) error {
|
||||
log.DefaultLogger.Info("service stopping")
|
||||
if as.discoverer != nil {
|
||||
as.discoverer.Stop()
|
||||
if ws.discoverer != nil {
|
||||
ws.discoverer.Stop()
|
||||
}
|
||||
if ws.server != nil {
|
||||
if err := ws.server.Shutdown(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.DefaultLogger.Info("service stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Health 检查服务健康状态,实现 starter.Service 接口
|
||||
func (ws *WebServer) Health() error {
|
||||
if ws.server == nil {
|
||||
return fmt.Errorf("server is not running")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload 实现配置重新加载,实现 starter.Reloader 接口
|
||||
func (ws *WebServer) Reload() error {
|
||||
log.DefaultLogger.Info("reloading configurations...")
|
||||
return triggerReload()
|
||||
}
|
||||
|
||||
// AsyncServer 兼容旧版异步服务实例
|
||||
type AsyncServer struct {
|
||||
*WebServer
|
||||
}
|
||||
|
||||
// Stop 兼容旧版的无参数停止方法
|
||||
func (as *AsyncServer) Stop() {
|
||||
stopTimeout := time.Duration(Config.StopTimeout) * time.Millisecond
|
||||
if stopTimeout <= 0 {
|
||||
stopTimeout = 5 * time.Second
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := as.server.Shutdown(ctx); err != nil {
|
||||
log.DefaultLogger.Error("server shutdown error", "error", err.Error())
|
||||
}
|
||||
log.DefaultLogger.Info("service stopped")
|
||||
_ = as.WebServer.Stop(ctx)
|
||||
}
|
||||
|
||||
// Wait 等待服务结束 (信号监听)
|
||||
// AsyncStart 兼容旧版的异步启动方法
|
||||
func AsyncStart() *AsyncServer {
|
||||
ws := NewWebServer()
|
||||
_ = ws.Start(context.Background(), log.DefaultLogger)
|
||||
return &AsyncServer{WebServer: ws}
|
||||
}
|
||||
|
||||
// Wait 等待服务结束 (兼容旧版,直接阻塞)
|
||||
func (as *AsyncServer) Wait() {
|
||||
<-as.stopChan
|
||||
as.Stop()
|
||||
select {}
|
||||
}
|
||||
|
||||
// Start 同步启动服务
|
||||
// Start 兼容旧版的同步启动方法 (通过内部注册 starter 实现)
|
||||
func Start() {
|
||||
AsyncStart().Wait()
|
||||
stopTimeout := time.Duration(Config.StopTimeout) * time.Millisecond
|
||||
if stopTimeout <= 0 {
|
||||
stopTimeout = 5 * time.Second
|
||||
}
|
||||
starter.Register("web-server", NewWebServer(), 100, 5*time.Second, stopTimeout)
|
||||
starter.Run()
|
||||
}
|
||||
|
||||
23
static.go
23
static.go
@ -43,6 +43,29 @@ func StaticByHost(path, rootPath, host string) {
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceStatics 使用 Copy-on-Write 机制原子地替换指定 host 下的所有静态目录规则
|
||||
func ReplaceStatics(host string, config map[string]string) {
|
||||
newStatics := make(map[string]*string, len(config))
|
||||
for path, rootPath := range config {
|
||||
rp := rootPath
|
||||
if !filepath.IsAbs(rp) {
|
||||
if absPath, err := filepath.Abs(rp); err == nil {
|
||||
rp = absPath
|
||||
}
|
||||
}
|
||||
newStatics[path] = &rp
|
||||
}
|
||||
|
||||
staticsByHostLock.Lock()
|
||||
defer staticsByHostLock.Unlock()
|
||||
|
||||
if host == "" {
|
||||
statics = newStatics
|
||||
} else {
|
||||
staticsByHost[host] = newStatics
|
||||
}
|
||||
}
|
||||
|
||||
func getStaticFilePath(requestPath, host string) string {
|
||||
staticsByHostLock.RLock()
|
||||
defer staticsByHostLock.RUnlock()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user