mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-12 12:16:37 +08:00
init
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/admission"
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/metrics"
|
||||
"github.com/go-gost/core/observer"
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
"github.com/go-gost/core/recorder"
|
||||
"github.com/go-gost/core/service"
|
||||
ctxvalue "github.com/go-gost/x/ctx"
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
xmetrics "github.com/go-gost/x/metrics"
|
||||
xstats "github.com/go-gost/x/observer/stats"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
admission admission.Admission
|
||||
recorders []recorder.RecorderObject
|
||||
preUp []string
|
||||
postUp []string
|
||||
preDown []string
|
||||
postDown []string
|
||||
stats stats.Stats
|
||||
observer observer.Observer
|
||||
observerPeriod time.Duration
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
type Option func(opts *options)
|
||||
|
||||
func AdmissionOption(admission admission.Admission) Option {
|
||||
return func(opts *options) {
|
||||
opts.admission = admission
|
||||
}
|
||||
}
|
||||
|
||||
func RecordersOption(recorders ...recorder.RecorderObject) Option {
|
||||
return func(opts *options) {
|
||||
opts.recorders = recorders
|
||||
}
|
||||
}
|
||||
|
||||
func PreUpOption(cmds []string) Option {
|
||||
return func(opts *options) {
|
||||
opts.preUp = cmds
|
||||
}
|
||||
}
|
||||
|
||||
func PreDownOption(cmds []string) Option {
|
||||
return func(opts *options) {
|
||||
opts.preDown = cmds
|
||||
}
|
||||
}
|
||||
|
||||
func PostUpOption(cmds []string) Option {
|
||||
return func(opts *options) {
|
||||
opts.postUp = cmds
|
||||
}
|
||||
}
|
||||
|
||||
func PostDownOption(cmds []string) Option {
|
||||
return func(opts *options) {
|
||||
opts.postDown = cmds
|
||||
}
|
||||
}
|
||||
|
||||
func StatsOption(stats stats.Stats) Option {
|
||||
return func(opts *options) {
|
||||
opts.stats = stats
|
||||
}
|
||||
}
|
||||
|
||||
func ObserverOption(observer observer.Observer) Option {
|
||||
return func(opts *options) {
|
||||
opts.observer = observer
|
||||
}
|
||||
}
|
||||
|
||||
func ObserverPeriodOption(period time.Duration) Option {
|
||||
return func(opts *options) {
|
||||
opts.observerPeriod = period
|
||||
}
|
||||
}
|
||||
|
||||
func LoggerOption(logger logger.Logger) Option {
|
||||
return func(opts *options) {
|
||||
opts.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
type defaultService struct {
|
||||
name string
|
||||
listener listener.Listener
|
||||
handler handler.Handler
|
||||
status *Status
|
||||
options options
|
||||
}
|
||||
|
||||
func NewService(name string, ln listener.Listener, h handler.Handler, opts ...Option) service.Service {
|
||||
var options options
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
s := &defaultService{
|
||||
name: name,
|
||||
listener: ln,
|
||||
handler: h,
|
||||
options: options,
|
||||
status: &Status{
|
||||
createTime: time.Now(),
|
||||
events: make([]Event, 0, MaxEventSize),
|
||||
stats: options.stats,
|
||||
},
|
||||
}
|
||||
s.setState(StateRunning)
|
||||
|
||||
s.execCmds("pre-up", s.options.preUp)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *defaultService) Addr() net.Addr {
|
||||
return s.listener.Addr()
|
||||
}
|
||||
|
||||
func (s *defaultService) Serve() error {
|
||||
s.execCmds("post-up", s.options.postUp)
|
||||
s.setState(StateReady)
|
||||
s.status.addEvent(Event{
|
||||
Time: time.Now(),
|
||||
Message: fmt.Sprintf("service %s is listening on %s", s.name, s.listener.Addr()),
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if s.status.Stats() != nil {
|
||||
go s.observeStats(ctx)
|
||||
}
|
||||
|
||||
if v := xmetrics.GetGauge(
|
||||
xmetrics.MetricServicesGauge,
|
||||
metrics.Labels{}); v != nil {
|
||||
v.Inc()
|
||||
defer v.Dec()
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
var tempDelay time.Duration
|
||||
for {
|
||||
conn, e := s.listener.Accept()
|
||||
if e != nil {
|
||||
// TODO: remove Temporary checking
|
||||
if ne, ok := e.(net.Error); ok && ne.Temporary() {
|
||||
if tempDelay == 0 {
|
||||
tempDelay = 1 * time.Second
|
||||
} else {
|
||||
tempDelay *= 2
|
||||
}
|
||||
if max := 5 * time.Second; tempDelay > max {
|
||||
tempDelay = max
|
||||
}
|
||||
|
||||
s.setState(StateFailed)
|
||||
|
||||
s.options.logger.Warnf("accept: %v, retrying in %v", e, tempDelay)
|
||||
time.Sleep(tempDelay)
|
||||
continue
|
||||
}
|
||||
s.setState(StateClosed)
|
||||
|
||||
if !errors.Is(e, net.ErrClosed) {
|
||||
s.options.logger.Errorf("accept: %v", e)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
if tempDelay > 0 {
|
||||
tempDelay = 0
|
||||
s.setState(StateReady)
|
||||
}
|
||||
|
||||
clientAddr := conn.RemoteAddr().String()
|
||||
if ca, ok := conn.(xnet.ClientAddr); ok {
|
||||
if addr := ca.ClientAddr(); addr != nil {
|
||||
clientAddr = addr.String()
|
||||
}
|
||||
}
|
||||
clientIP := clientAddr
|
||||
if h, _, _ := net.SplitHostPort(clientAddr); h != "" {
|
||||
clientIP = h
|
||||
}
|
||||
|
||||
sid := xid.New().String()
|
||||
ctx := ctxvalue.ContextWithSid(ctx, ctxvalue.Sid(sid))
|
||||
ctx = ctxvalue.ContextWithClientAddr(ctx, ctxvalue.ClientAddr(clientAddr))
|
||||
ctx = ctxvalue.ContextWithHash(ctx, &ctxvalue.Hash{Source: clientIP})
|
||||
|
||||
log := s.options.logger.WithFields(map[string]any{
|
||||
"sid": sid,
|
||||
})
|
||||
|
||||
for _, rec := range s.options.recorders {
|
||||
if rec.Record == recorder.RecorderServiceClientAddress {
|
||||
if err := rec.Recorder.Record(ctx, []byte(clientIP)); err != nil {
|
||||
log.Errorf("record %s: %v", rec.Record, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if s.options.admission != nil &&
|
||||
!s.options.admission.Admit(ctx, clientAddr) {
|
||||
conn.Close()
|
||||
log.Debugf("admission: %s is denied", clientAddr)
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if v := xmetrics.GetCounter(xmetrics.MetricServiceRequestsCounter,
|
||||
metrics.Labels{"service": s.name, "client": clientIP}); v != nil {
|
||||
v.Inc()
|
||||
}
|
||||
|
||||
if v := xmetrics.GetGauge(xmetrics.MetricServiceRequestsInFlightGauge,
|
||||
metrics.Labels{"service": s.name, "client": clientIP}); v != nil {
|
||||
v.Inc()
|
||||
defer v.Dec()
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
if v := xmetrics.GetObserver(xmetrics.MetricServiceRequestsDurationObserver,
|
||||
metrics.Labels{"service": s.name}); v != nil {
|
||||
defer func() {
|
||||
v.Observe(float64(time.Since(start).Seconds()))
|
||||
}()
|
||||
}
|
||||
|
||||
if err := s.handler.Handle(ctx, conn); err != nil {
|
||||
log.Error(err)
|
||||
if v := xmetrics.GetCounter(xmetrics.MetricServiceHandlerErrorsCounter,
|
||||
metrics.Labels{"service": s.name, "client": clientIP}); v != nil {
|
||||
v.Inc()
|
||||
}
|
||||
if sts := s.status.stats; sts != nil {
|
||||
sts.Add(stats.KindTotalErrs, 1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *defaultService) Status() *Status {
|
||||
return s.status
|
||||
}
|
||||
|
||||
func (s *defaultService) Close() error {
|
||||
s.execCmds("pre-down", s.options.preDown)
|
||||
defer s.execCmds("post-down", s.options.postDown)
|
||||
|
||||
if closer, ok := s.handler.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
return s.listener.Close()
|
||||
}
|
||||
|
||||
func (s *defaultService) execCmds(phase string, cmds []string) {
|
||||
for _, cmd := range cmds {
|
||||
cmd := strings.TrimSpace(cmd)
|
||||
if cmd == "" {
|
||||
continue
|
||||
}
|
||||
s.options.logger.Info(cmd)
|
||||
|
||||
if err := exec.Command("/bin/sh", "-c", cmd).Run(); err != nil {
|
||||
s.options.logger.Warnf("[%s] %s: %v", phase, cmd, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *defaultService) setState(state State) {
|
||||
s.status.setState(state)
|
||||
|
||||
msg := fmt.Sprintf("service %s is %s", s.name, state)
|
||||
s.status.addEvent(Event{
|
||||
Time: time.Now(),
|
||||
Message: msg,
|
||||
})
|
||||
|
||||
if obs := s.options.observer; obs != nil {
|
||||
obs.Observe(context.Background(), []observer.Event{ServiceEvent{
|
||||
Kind: "service",
|
||||
Service: s.name,
|
||||
State: state,
|
||||
Msg: msg,
|
||||
}})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *defaultService) observeStats(ctx context.Context) {
|
||||
if s.options.observer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
d := s.options.observerPeriod
|
||||
if d == 0 {
|
||||
d = 5 * time.Second
|
||||
}
|
||||
if d < time.Second {
|
||||
d = 1 * time.Second
|
||||
}
|
||||
|
||||
var events []observer.Event
|
||||
|
||||
ticker := time.NewTicker(d)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if len(events) > 0 {
|
||||
if err := s.options.observer.Observe(ctx, events); err == nil {
|
||||
events = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
st := s.status.Stats()
|
||||
if st == nil || !st.IsUpdated() {
|
||||
break
|
||||
}
|
||||
|
||||
evs := []observer.Event{
|
||||
xstats.StatsEvent{
|
||||
Kind: "service",
|
||||
Service: s.name,
|
||||
TotalConns: st.Get(stats.KindTotalConns),
|
||||
CurrentConns: st.Get(stats.KindCurrentConns),
|
||||
InputBytes: st.Get(stats.KindInputBytes),
|
||||
OutputBytes: st.Get(stats.KindOutputBytes),
|
||||
TotalErrs: st.Get(stats.KindTotalErrs),
|
||||
},
|
||||
}
|
||||
if err := s.options.observer.Observe(ctx, evs); err != nil {
|
||||
events = evs
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ServiceEvent struct {
|
||||
Kind string
|
||||
Service string
|
||||
State State
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (ServiceEvent) Type() observer.EventType {
|
||||
return observer.EventStatus
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxEventSize = 20
|
||||
)
|
||||
|
||||
type State string
|
||||
|
||||
const (
|
||||
StateRunning State = "running"
|
||||
StateReady State = "ready"
|
||||
StateFailed State = "failed"
|
||||
StateClosed State = "closed"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Time time.Time
|
||||
Message string
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
createTime time.Time
|
||||
state State
|
||||
events []Event
|
||||
stats stats.Stats
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (p *Status) CreateTime() time.Time {
|
||||
return p.createTime
|
||||
}
|
||||
|
||||
func (p *Status) State() State {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.state
|
||||
}
|
||||
|
||||
func (p *Status) setState(state State) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.state = state
|
||||
}
|
||||
|
||||
func (p *Status) Events() []Event {
|
||||
events := make([]Event, MaxEventSize)
|
||||
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
copy(events, p.events)
|
||||
return events
|
||||
}
|
||||
|
||||
func (p *Status) addEvent(event Event) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if len(p.events) == MaxEventSize {
|
||||
events := make([]Event, MaxEventSize-1, MaxEventSize)
|
||||
copy(events, p.events[1:])
|
||||
p.events = events
|
||||
}
|
||||
p.events = append(p.events, event)
|
||||
}
|
||||
|
||||
func (p *Status) Stats() stats.Stats {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p.stats
|
||||
}
|
||||
Reference in New Issue
Block a user