webapi: Split Start func into New and Run funcs.

Separating the single func grants the calling code greater control over
the webapi lifecycle.
This commit is contained in:
jholdstock 2023-09-15 11:52:35 +01:00 committed by Jamie Holdstock
parent a7bb0cd9d7
commit 0742e0ff1a
2 changed files with 38 additions and 35 deletions

View File

@ -136,10 +136,7 @@ func (v *vspd) run() int {
return 0 return 0
} }
// WaitGroup for services to signal when they have shutdown cleanly. // Create webapi server.
var shutdownWg sync.WaitGroup
// Create and start webapi server.
apiCfg := webapi.Config{ apiCfg := webapi.Config{
Listen: v.cfg.Listen, Listen: v.cfg.Listen,
VSPFee: v.cfg.VSPFee, VSPFee: v.cfg.VSPFee,
@ -153,15 +150,17 @@ func (v *vspd) run() int {
MaxVoteChangeRecords: maxVoteChangeRecords, MaxVoteChangeRecords: maxVoteChangeRecords,
VspdVersion: version.String(), VspdVersion: version.String(),
} }
err = webapi.Start(ctx, requestShutdown, &shutdownWg, v.db, v.cfg.logger("API"), api, err := webapi.New(v.db, v.cfg.logger("API"), v.dcrd, v.wallets, apiCfg)
v.dcrd, v.wallets, apiCfg)
if err != nil { if err != nil {
v.log.Errorf("Failed to initialize webapi: %v", err) v.log.Errorf("Failed to initialize webapi: %v", err)
requestShutdown()
shutdownWg.Wait()
return 1 return 1
} }
// WaitGroup for services to signal when they have shutdown cleanly.
var shutdownWg sync.WaitGroup
api.Run(ctx, requestShutdown, &shutdownWg)
// Start all background tasks and notification handlers. // Start all background tasks and notification handlers.
shutdownWg.Add(1) shutdownWg.Add(1)
go func() { go func() {

View File

@ -73,16 +73,17 @@ type WebAPI struct {
cache *cache cache *cache
signPrivKey ed25519.PrivateKey signPrivKey ed25519.PrivateKey
signPubKey ed25519.PublicKey signPubKey ed25519.PublicKey
server *http.Server
listener net.Listener
} }
func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGroup, func New(vdb *database.VspDatabase, log slog.Logger, dcrd rpc.DcrdConnect,
vdb *database.VspDatabase, log slog.Logger, dcrd rpc.DcrdConnect, wallets rpc.WalletConnect, cfg Config) (*WebAPI, error) {
wallets rpc.WalletConnect, cfg Config) error {
// Get keys for signing API responses from the database. // Get keys for signing API responses from the database.
signPrivKey, signPubKey, err := vdb.KeyPair() signPrivKey, signPubKey, err := vdb.KeyPair()
if err != nil { if err != nil {
return fmt.Errorf("db.Keypair error: %w", err) return nil, fmt.Errorf("db.Keypair error: %w", err)
} }
// Populate cached VSP stats before starting webserver. // Populate cached VSP stats before starting webserver.
@ -97,23 +98,30 @@ func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGro
// use them to initialize the address generator. // use them to initialize the address generator.
idx, err := vdb.GetLastAddressIndex() idx, err := vdb.GetLastAddressIndex()
if err != nil { if err != nil {
return fmt.Errorf("db.GetLastAddressIndex error: %w", err) return nil, fmt.Errorf("db.GetLastAddressIndex error: %w", err)
} }
feeXPub, err := vdb.FeeXPub() feeXPub, err := vdb.FeeXPub()
if err != nil { if err != nil {
return fmt.Errorf("db.GetFeeXPub error: %w", err) return nil, fmt.Errorf("db.GetFeeXPub error: %w", err)
} }
addrGen, err := newAddressGenerator(feeXPub, cfg.Network.Params, idx, log) addrGen, err := newAddressGenerator(feeXPub, cfg.Network.Params, idx, log)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize fee address generator: %w", err) return nil, fmt.Errorf("failed to initialize fee address generator: %w", err)
} }
// Get the secret key used to initialize the cookie store. // Get the secret key used to initialize the cookie store.
cookieSecret, err := vdb.CookieSecret() cookieSecret, err := vdb.CookieSecret()
if err != nil { if err != nil {
return fmt.Errorf("db.GetCookieSecret error: %w", err) return nil, fmt.Errorf("db.GetCookieSecret error: %w", err)
} }
// Create TCP listener.
listener, err := net.Listen("tcp", cfg.Listen)
if err != nil {
return nil, err
}
log.Infof("Listening on %s", cfg.Listen)
w := &WebAPI{ w := &WebAPI{
cfg: cfg, cfg: cfg,
db: vdb, db: vdb,
@ -122,48 +130,46 @@ func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGro
cache: cache, cache: cache,
signPrivKey: signPrivKey, signPrivKey: signPrivKey,
signPubKey: signPubKey, signPubKey: signPubKey,
listener: listener,
} }
// Create TCP listener. w.server = &http.Server{
var listenConfig net.ListenConfig
listener, err := listenConfig.Listen(ctx, "tcp", cfg.Listen)
if err != nil {
return err
}
log.Infof("Listening on %s", cfg.Listen)
srv := http.Server{
Handler: w.router(cookieSecret, dcrd, wallets), Handler: w.router(cookieSecret, dcrd, wallets),
ReadTimeout: 5 * time.Second, // slow requests should not hold connections opened ReadTimeout: 5 * time.Second, // slow requests should not hold connections opened
WriteTimeout: 60 * time.Second, // hung responses must die WriteTimeout: 60 * time.Second, // hung responses must die
} }
return w, nil
}
func (w *WebAPI) Run(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGroup) {
// Add the graceful shutdown to the waitgroup. // Add the graceful shutdown to the waitgroup.
shutdownWg.Add(1) shutdownWg.Add(1)
go func() { go func() {
// Wait until shutdown is signaled before shutting down. // Wait until shutdown is signaled before shutting down.
<-ctx.Done() <-ctx.Done()
log.Debug("Stopping webserver...") w.log.Debug("Stopping webserver...")
// Give the webserver 5 seconds to finish what it is doing. // Give the webserver 10 seconds to finish what it is doing.
timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
if err := srv.Shutdown(timeoutCtx); err != nil { if err := w.server.Shutdown(timeoutCtx); err != nil {
log.Errorf("Failed to stop webserver cleanly: %v", err) w.log.Errorf("Failed to stop webserver cleanly: %v", err)
} else { } else {
log.Debug("Webserver stopped") w.log.Debug("Webserver stopped")
} }
shutdownWg.Done() shutdownWg.Done()
}() }()
// Start webserver. // Start webserver.
go func() { go func() {
err := srv.Serve(listener) err := w.server.Serve(w.listener)
// If the server dies for any reason other than ErrServerClosed (from // If the server dies for any reason other than ErrServerClosed (from
// graceful server.Shutdown), log the error and request vspd be // graceful server.Shutdown), log the error and request vspd be
// shutdown. // shutdown.
if err != nil && !errors.Is(err, http.ErrServerClosed) { if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Errorf("Unexpected webserver error: %v", err) w.log.Errorf("Unexpected webserver error: %v", err)
requestShutdown() requestShutdown()
} }
}() }()
@ -185,13 +191,11 @@ func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGro
case <-time.After(refresh): case <-time.After(refresh):
err := w.cache.update() err := w.cache.update()
if err != nil { if err != nil {
log.Errorf("Failed to update cached VSP stats: %v", err) w.log.Errorf("Failed to update cached VSP stats: %v", err)
} }
} }
} }
}() }()
return nil
} }
func (w *WebAPI) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.WalletConnect) *gin.Engine { func (w *WebAPI) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.WalletConnect) *gin.Engine {