Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

Commit 757be51

Browse files
committed
upload-proxy: add reload
1 parent dd7d580 commit 757be51

6 files changed

Lines changed: 267 additions & 100 deletions

File tree

cmd/upload-proxy/config.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,39 @@ import (
1010
type Config struct {
1111
ListenAddress string
1212
Sinks []SinkConfig
13+
Auth AuthConfig
1314
}
1415

15-
func (c *Config) ReadFromFile(path string) error {
16+
type SinkConfig struct {
17+
Address string
18+
QueueSize int `toml:"queue-size"`
19+
AuthConfig
20+
}
21+
22+
func (s *SinkConfig) ApplyDefaults() {
23+
if s.QueueSize == 0 {
24+
s.QueueSize = DefaultQueueSize
25+
}
26+
if s.Username != "" && (s.AuthType != AuthTypeBasic && s.AuthType != AuthTypeDigest) {
27+
s.AuthType = AuthTypeBasic
28+
}
29+
}
30+
31+
type AuthConfig struct {
32+
Username string
33+
Password string
34+
AuthType AuthType `toml:"auth-type"`
35+
}
36+
37+
type AuthType string
38+
39+
const (
40+
AuthTypeNone AuthType = "none"
41+
AuthTypeBasic AuthType = "basic"
42+
AuthTypeDigest AuthType = "digest"
43+
)
44+
45+
func (c *Config) Load(path string, authPath string) error {
1646
data, err := os.ReadFile(path)
1747
if err != nil {
1848
return fmt.Errorf("read: %w", err)
@@ -21,5 +51,23 @@ func (c *Config) ReadFromFile(path string) error {
2151
if err != nil {
2252
return fmt.Errorf("unmarshal: %w", err)
2353
}
54+
// load separate auth config if specified
55+
if authPath != "" {
56+
data, err := os.ReadFile(authPath)
57+
if err != nil {
58+
return fmt.Errorf("read: %w", err)
59+
}
60+
err = toml.Unmarshal(data, &c.Auth)
61+
if err != nil {
62+
return fmt.Errorf("unmarshal: %w", err)
63+
}
64+
}
65+
// apply auth config to all sinks if set
66+
for i := range c.Sinks {
67+
c.Sinks[i].ApplyDefaults()
68+
if c.Auth.Username != "" {
69+
c.Sinks[i].AuthConfig = c.Auth
70+
}
71+
}
2472
return nil
2573
}

cmd/upload-proxy/main.go

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,22 @@ import (
55
"flag"
66
"fmt"
77
"os"
8+
"os/signal"
9+
"syscall"
810

911
"github.com/rs/zerolog"
1012
"github.com/rs/zerolog/log"
11-
"github.com/voc/stream-api/util"
1213
)
1314

1415
func main() {
1516
conf := Config{}
16-
config := flag.String("config", "config.toml", "Set path to proxy config")
17+
configPath := flag.String("config", "config.toml", "Set path to proxy config")
18+
authConfigPath := flag.String("auth-config", "", "Set path to separate auth config (optional)")
1719
debug := flag.Bool("debug", false, "sets log level to debug")
1820
flag.StringVar(&conf.ListenAddress, "addr", ":8080", "Set listen address")
1921
flag.Parse()
2022
ctx, cancel := context.WithCancel(context.Background())
2123
defer cancel()
22-
util.HandleSignal(ctx, cancel)
2324

2425
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
2526

@@ -28,42 +29,46 @@ func main() {
2829
zerolog.SetGlobalLevel(zerolog.DebugLevel)
2930
}
3031

31-
if err := conf.ReadFromFile(*config); err != nil {
32+
if err := conf.Load(*configPath, *authConfigPath); err != nil {
3233
log.Fatal().Err(err).Msg("config parse failed")
3334
}
3435

3536
// Run proxy
36-
if err := run(ctx, &conf); err != nil {
37+
if err := run(ctx, conf, *configPath, *authConfigPath); err != nil {
3738
log.Fatal().Err(err).Msg("proxy run failed")
3839
}
3940
}
4041

41-
func run(parentCtx context.Context, conf *Config) error {
42+
func run(parentCtx context.Context, conf Config, configPath string, authConfigPath string) error {
4243
ctx, cancel := context.WithCancel(parentCtx)
4344
defer cancel()
44-
var sinks []*Sink
45-
if len(conf.Sinks) == 0 {
46-
log.Warn().Msg("no sinks configured")
47-
}
48-
for _, sinkConfig := range conf.Sinks {
49-
sink, err := NewSink(sinkConfig)
50-
if err != nil {
51-
return fmt.Errorf("sink init failed: %w", err)
52-
}
53-
sinks = append(sinks, sink)
54-
log.Info().Str("sink", sink.url.Host).Str("basePath", sink.url.Path).Str("authType", string(sink.conf.AuthType)).Msg("added sink")
55-
}
56-
proxy, err := NewProxy(ctx, conf.ListenAddress, sinks)
45+
proxy, err := NewProxy(ctx, conf)
5746
if err != nil {
5847
return fmt.Errorf("proxy init failed: %w", err)
5948
}
6049
log.Info().Msgf("listening on %s", conf.ListenAddress)
6150

62-
select {
63-
case <-ctx.Done():
64-
case err := <-proxy.Errors():
65-
log.Error().Err(err).Msg("server failed")
66-
cancel()
51+
signalReload := make(chan os.Signal, 1)
52+
signal.Notify(signalReload, syscall.SIGHUP)
53+
54+
outer:
55+
for {
56+
select {
57+
case <-ctx.Done():
58+
break outer
59+
case err := <-proxy.Errors():
60+
log.Error().Err(err).Msg("server failed")
61+
cancel()
62+
break outer
63+
case <-signalReload:
64+
log.Info().Msg("reloading config")
65+
var newConf Config
66+
if err := newConf.Load(configPath, authConfigPath); err != nil {
67+
log.Error().Err(err).Msg("config parse failed")
68+
break
69+
}
70+
_ = proxy.UpdateConfig(ctx, newConf)
71+
}
6772
}
6873

6974
proxy.Wait()

cmd/upload-proxy/main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ func TestSmoke(t *testing.T) {
1010
t.Parallel()
1111
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
1212
defer cancel()
13-
err := run(ctx, &Config{
13+
err := run(ctx, Config{
1414
ListenAddress: "127.0.0.1:0",
1515
Sinks: []SinkConfig{{
1616
Address: "http://1.2.3.4:5678/upload",
1717
}},
18-
})
18+
}, "", "")
1919
if err != nil {
2020
t.Error(err)
2121
}

cmd/upload-proxy/proxy.go

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const MaxFileSize = 50 * 1024 * 1024 // 50 MB
2424

2525
type Proxy struct {
2626
sinks []*Sink
27+
sinkMutex sync.Mutex
2728
errors chan error
2829
transport *http.Transport
2930
ctx context.Context
@@ -32,7 +33,7 @@ type Proxy struct {
3233
metrics *ProxyMetrics
3334
}
3435

35-
func NewProxy(ctx context.Context, addr string, sinks []*Sink) (*Proxy, error) {
36+
func NewProxy(ctx context.Context, conf Config) (*Proxy, error) {
3637
tr := &http.Transport{
3738
DialContext: (&net.Dialer{
3839
Timeout: 30 * time.Second,
@@ -44,27 +45,25 @@ func NewProxy(ctx context.Context, addr string, sinks []*Sink) (*Proxy, error) {
4445
TLSHandshakeTimeout: 10 * time.Second,
4546
ExpectContinueTimeout: 1 * time.Second,
4647
}
48+
reg := prometheus.NewRegistry()
49+
reg.MustRegister(
50+
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
51+
)
4752
p := &Proxy{
48-
sinks: sinks,
4953
transport: tr,
5054
errors: make(chan error, 1),
5155
ctx: ctx,
56+
metrics: NewProxyMetrics(reg),
5257
}
5358

5459
mux := http.NewServeMux()
55-
srv := http.Server{Addr: addr, Handler: mux}
56-
57-
reg := prometheus.NewRegistry()
58-
reg.MustRegister(
59-
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
60-
)
61-
p.metrics = NewProxyMetrics(reg)
60+
srv := http.Server{Addr: conf.ListenAddress, Handler: mux}
6261

6362
// set routes
6463
mux.HandleFunc("/", p.HandleUpload)
6564
mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
6665

67-
ln, err := net.Listen("tcp", addr)
66+
ln, err := net.Listen("tcp", conf.ListenAddress)
6867
if err != nil {
6968
return nil, err
7069
}
@@ -91,22 +90,100 @@ func NewProxy(ctx context.Context, addr string, sinks []*Sink) (*Proxy, error) {
9190
}
9291
}()
9392

94-
// run sink uploaders
95-
for _, sink := range p.sinks {
96-
log.Printf("setup sink %+v\n", sink)
93+
p.done.Add(1)
94+
go p.runTimeout(ctx)
9795

98-
// if the number of workers is >1 the server would have to deal with out of order playlists
99-
sink.Start(ctx, p.transport, reg, 1)
96+
// initial config update
97+
if err := p.UpdateConfig(ctx, conf); err != nil {
98+
return nil, err
10099
}
101100

102101
return p, nil
103102
}
104103

104+
func (p *Proxy) UpdateConfig(ctx context.Context, conf Config) error {
105+
p.sinkMutex.Lock()
106+
defer p.sinkMutex.Unlock()
107+
var newConfigs []SinkConfig
108+
var err error
109+
// mark removed sinks for graceful deletion
110+
for _, s := range p.sinks {
111+
var found bool
112+
for _, sinkConfig := range conf.Sinks {
113+
if s.Address() == sinkConfig.Address {
114+
found = true
115+
break
116+
}
117+
}
118+
if found {
119+
continue
120+
}
121+
s.StartGracePeriod()
122+
}
123+
// update existing sinks
124+
for _, sinkConfig := range conf.Sinks {
125+
var updated bool
126+
for _, s := range p.sinks {
127+
if s.Address() != sinkConfig.Address {
128+
continue
129+
}
130+
// update existing sink
131+
if err2 := s.UpdateConfig(ctx, sinkConfig); err2 != nil {
132+
log.Error().Err(err2).Str("sink", s.url.Host).Msg("sink update failed")
133+
firstErr(err2, &err)
134+
}
135+
updated = true
136+
break
137+
}
138+
if updated {
139+
continue
140+
}
141+
newConfigs = append(newConfigs, sinkConfig)
142+
}
143+
// create new sinks
144+
for _, sinkConfig := range newConfigs {
145+
sink, err2 := NewSink(ctx, sinkConfig, p.transport, p.metrics.reg)
146+
if err2 != nil {
147+
firstErr(err2, &err)
148+
log.Error().Err(err2).Str("sink", sinkConfig.Address).Msg("sink init failed")
149+
continue
150+
}
151+
p.sinks = append(p.sinks, sink)
152+
log.Info().Str("sink", sink.url.Host).Str("basePath", sink.url.Path).Str("authType", string(sink.conf.AuthType)).Msg("added sink")
153+
}
154+
return err
155+
}
156+
157+
func (p *Proxy) runTimeout(ctx context.Context) {
158+
defer p.done.Done()
159+
ticker := time.NewTicker(time.Minute)
160+
defer ticker.Stop()
161+
for {
162+
select {
163+
case <-ctx.Done():
164+
return
165+
case <-ticker.C:
166+
p.sinkMutex.Lock()
167+
sinks := p.sinks[:0]
168+
for _, s := range p.sinks {
169+
if s.IsStale() {
170+
s.Stop()
171+
log.Info().Str("sink", s.url.Host).Msg("removed sink after timeout")
172+
continue
173+
}
174+
sinks = append(sinks, s)
175+
}
176+
p.sinks = sinks
177+
p.sinkMutex.Unlock()
178+
}
179+
}
180+
}
181+
105182
// Wait for server to finish
106183
func (p *Proxy) Wait() {
107184
p.done.Wait()
108185
for _, sink := range p.sinks {
109-
sink.wait()
186+
sink.Stop()
110187
}
111188
p.metrics.deregister()
112189
}
@@ -235,3 +312,9 @@ func (m *ProxyMetrics) deregister() {
235312
m.reg.Unregister(m.totalNumRead)
236313
m.reg.Unregister(m.totalReadDelay)
237314
}
315+
316+
func firstErr(err error, errPtr *error) {
317+
if *errPtr == nil && err != nil {
318+
*errPtr = err
319+
}
320+
}

cmd/upload-proxy/proxy_test.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,15 @@ func TestUpload(t *testing.T) {
2323
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
2424
defer cancel()
2525

26-
sink, err := NewSink(SinkConfig{
27-
Address: srv.URL + "/upload",
26+
proxy, err := NewProxy(ctx, Config{
27+
ListenAddress: "127.0.0.1:0",
28+
Sinks: []SinkConfig{{
29+
Address: srv.URL + "/upload",
30+
}},
2831
})
2932
if err != nil {
3033
t.Fatal(err)
3134
}
32-
33-
proxy, err := NewProxy(ctx, "127.0.0.1:0", []*Sink{sink})
34-
if err != nil {
35-
t.Fatal(err)
36-
}
3735
go func() {
3836
select {
3937
case <-ctx.Done():

0 commit comments

Comments
 (0)