2019-03-18 10:55:06 +00:00
|
|
|
// Copyright (C) 2019 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
package orders
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2019-03-21 13:24:26 +00:00
|
|
|
"io"
|
2019-12-30 20:35:26 +00:00
|
|
|
"math/rand"
|
2019-03-18 10:55:06 +00:00
|
|
|
"time"
|
|
|
|
|
2019-11-08 20:40:39 +00:00
|
|
|
"github.com/spacemonkeygo/monkit/v3"
|
2019-06-04 13:31:39 +01:00
|
|
|
"github.com/zeebo/errs"
|
2019-03-18 10:55:06 +00:00
|
|
|
"go.uber.org/zap"
|
2019-03-21 13:24:26 +00:00
|
|
|
"golang.org/x/sync/errgroup"
|
2019-03-18 10:55:06 +00:00
|
|
|
|
2019-12-27 11:48:47 +00:00
|
|
|
"storj.io/common/pb"
|
|
|
|
"storj.io/common/rpc"
|
|
|
|
"storj.io/common/storj"
|
|
|
|
"storj.io/common/sync2"
|
2019-07-18 18:15:09 +01:00
|
|
|
"storj.io/storj/storagenode/trust"
|
2019-03-18 10:55:06 +00:00
|
|
|
)
|
|
|
|
|
2019-06-04 13:31:39 +01:00
|
|
|
var (
|
|
|
|
// OrderError represents errors with orders
|
|
|
|
OrderError = errs.Class("order")
|
2019-08-16 15:53:22 +01:00
|
|
|
// OrderNotFoundError is the error returned when an order is not found
|
|
|
|
OrderNotFoundError = errs.Class("order not found")
|
2019-06-04 13:31:39 +01:00
|
|
|
|
|
|
|
mon = monkit.Package()
|
|
|
|
)
|
|
|
|
|
2019-03-18 10:55:06 +00:00
|
|
|
// Info contains full information about an order.
|
|
|
|
type Info struct {
|
2019-07-09 22:33:45 +01:00
|
|
|
Limit *pb.OrderLimit
|
|
|
|
Order *pb.Order
|
2019-03-18 10:55:06 +00:00
|
|
|
}
|
|
|
|
|
2019-03-21 13:24:26 +00:00
|
|
|
// ArchivedInfo contains full information about an archived order.
|
|
|
|
type ArchivedInfo struct {
|
2019-07-09 22:33:45 +01:00
|
|
|
Limit *pb.OrderLimit
|
|
|
|
Order *pb.Order
|
2019-03-21 13:24:26 +00:00
|
|
|
|
|
|
|
Status Status
|
|
|
|
ArchivedAt time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// Status is the archival status of the order.
|
|
|
|
type Status byte
|
|
|
|
|
|
|
|
// Statuses for satellite responses.
|
|
|
|
const (
|
|
|
|
StatusUnsent Status = iota
|
|
|
|
StatusAccepted
|
|
|
|
StatusRejected
|
|
|
|
)
|
|
|
|
|
2019-07-31 17:40:08 +01:00
|
|
|
// ArchiveRequest defines arguments for archiving a single order.
|
|
|
|
type ArchiveRequest struct {
|
|
|
|
Satellite storj.NodeID
|
|
|
|
Serial storj.SerialNumber
|
|
|
|
Status Status
|
|
|
|
}
|
|
|
|
|
2019-03-18 10:55:06 +00:00
|
|
|
// DB implements storing orders for sending to the satellite.
|
2019-09-10 14:24:16 +01:00
|
|
|
//
|
|
|
|
// architecture: Database
|
2019-03-18 10:55:06 +00:00
|
|
|
type DB interface {
|
|
|
|
// Enqueue inserts order to the list of orders needing to be sent to the satellite.
|
|
|
|
Enqueue(ctx context.Context, info *Info) error
|
|
|
|
// ListUnsent returns orders that haven't been sent yet.
|
|
|
|
ListUnsent(ctx context.Context, limit int) ([]*Info, error)
|
2019-03-21 13:24:26 +00:00
|
|
|
// ListUnsentBySatellite returns orders that haven't been sent yet grouped by satellite.
|
|
|
|
ListUnsentBySatellite(ctx context.Context) (map[storj.NodeID][]*Info, error)
|
|
|
|
|
|
|
|
// Archive marks order as being handled.
|
2019-08-22 15:33:14 +01:00
|
|
|
Archive(ctx context.Context, archivedAt time.Time, requests ...ArchiveRequest) error
|
2019-03-21 13:24:26 +00:00
|
|
|
// ListArchived returns orders that have been sent.
|
|
|
|
ListArchived(ctx context.Context, limit int) ([]*ArchivedInfo, error)
|
2019-08-15 17:56:33 +01:00
|
|
|
// CleanArchive deletes all entries older than ttl
|
|
|
|
CleanArchive(ctx context.Context, ttl time.Duration) (int, error)
|
2019-03-18 10:55:06 +00:00
|
|
|
}
|
|
|
|
|
2019-08-22 15:33:14 +01:00
|
|
|
// Config defines configuration for sending orders.
|
|
|
|
type Config struct {
|
2019-12-30 20:35:26 +00:00
|
|
|
MaxSleep time.Duration `help:"maximum duration to wait before trying to send orders" releaseDefault:"300s" devDefault:"1s"`
|
2019-10-23 00:57:24 +01:00
|
|
|
SenderInterval time.Duration `help:"duration between sending" default:"1h0m0s"`
|
|
|
|
SenderTimeout time.Duration `help:"timeout for sending" default:"1h0m0s"`
|
|
|
|
SenderDialTimeout time.Duration `help:"timeout for dialing satellite during sending orders" default:"1m0s"`
|
2019-12-15 22:41:22 +00:00
|
|
|
CleanupInterval time.Duration `help:"duration between archive cleanups" default:"1h0m0s"`
|
2019-10-23 00:57:24 +01:00
|
|
|
ArchiveTTL time.Duration `help:"length of time to archive orders before deletion" default:"168h0m0s"` // 7 days
|
2019-03-18 10:55:06 +00:00
|
|
|
}
|
|
|
|
|
2019-08-22 15:33:14 +01:00
|
|
|
// Service sends every interval unsent orders to the satellite.
|
2019-09-10 14:24:16 +01:00
|
|
|
//
|
|
|
|
// architecture: Chore
|
2019-08-22 15:33:14 +01:00
|
|
|
type Service struct {
|
2019-03-18 10:55:06 +00:00
|
|
|
log *zap.Logger
|
2019-08-22 15:33:14 +01:00
|
|
|
config Config
|
2019-03-18 10:55:06 +00:00
|
|
|
|
2019-09-19 05:46:39 +01:00
|
|
|
dialer rpc.Dialer
|
|
|
|
orders DB
|
|
|
|
trust *trust.Pool
|
2019-03-21 13:24:26 +00:00
|
|
|
|
2020-01-29 15:37:50 +00:00
|
|
|
Sender *sync2.Cycle
|
|
|
|
Cleanup *sync2.Cycle
|
2019-03-18 10:55:06 +00:00
|
|
|
}
|
|
|
|
|
2019-08-22 15:33:14 +01:00
|
|
|
// NewService creates an order service.
|
2019-09-19 05:46:39 +01:00
|
|
|
func NewService(log *zap.Logger, dialer rpc.Dialer, orders DB, trust *trust.Pool, config Config) *Service {
|
2019-08-22 15:33:14 +01:00
|
|
|
return &Service{
|
2019-09-19 05:46:39 +01:00
|
|
|
log: log,
|
|
|
|
dialer: dialer,
|
|
|
|
orders: orders,
|
|
|
|
config: config,
|
|
|
|
trust: trust,
|
2019-03-21 13:24:26 +00:00
|
|
|
|
2020-01-29 15:37:50 +00:00
|
|
|
Sender: sync2.NewCycle(config.SenderInterval),
|
|
|
|
Cleanup: sync2.NewCycle(config.CleanupInterval),
|
2019-03-18 10:55:06 +00:00
|
|
|
}
|
|
|
|
}
|
2019-03-21 13:24:26 +00:00
|
|
|
|
|
|
|
// Run sends orders on every interval to the appropriate satellites.
|
2019-08-22 15:33:14 +01:00
|
|
|
func (service *Service) Run(ctx context.Context) (err error) {
|
2019-06-04 13:31:39 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-08-22 15:33:14 +01:00
|
|
|
|
|
|
|
var group errgroup.Group
|
2019-12-30 20:35:26 +00:00
|
|
|
|
|
|
|
service.Sender.Start(ctx, &group, func(ctx context.Context) error {
|
|
|
|
if err := service.sleep(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err := service.sendOrders(ctx)
|
|
|
|
if err != nil {
|
|
|
|
service.log.Error("sending orders failed", zap.Error(err))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
service.Cleanup.Start(ctx, &group, func(ctx context.Context) error {
|
|
|
|
if err := service.sleep(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err := service.cleanArchive(ctx)
|
|
|
|
if err != nil {
|
|
|
|
service.log.Error("clean archive failed", zap.Error(err))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
2019-08-22 15:33:14 +01:00
|
|
|
|
|
|
|
return group.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (service *Service) cleanArchive(ctx context.Context) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
service.log.Debug("cleaning")
|
|
|
|
|
|
|
|
deleted, err := service.orders.CleanArchive(ctx, service.config.ArchiveTTL)
|
|
|
|
if err != nil {
|
|
|
|
service.log.Error("cleaning archive", zap.Error(err))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
service.log.Debug("cleanup finished", zap.Int("items deleted", deleted))
|
|
|
|
return nil
|
2019-06-04 13:31:39 +01:00
|
|
|
}
|
2019-03-21 13:24:26 +00:00
|
|
|
|
2019-08-22 15:33:14 +01:00
|
|
|
func (service *Service) sendOrders(ctx context.Context) (err error) {
|
2019-06-04 13:31:39 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-08-22 15:33:14 +01:00
|
|
|
service.log.Debug("sending")
|
2019-03-21 13:24:26 +00:00
|
|
|
|
2019-07-31 17:40:08 +01:00
|
|
|
const batchSize = 1000
|
|
|
|
|
2019-08-22 15:33:14 +01:00
|
|
|
ordersBySatellite, err := service.orders.ListUnsentBySatellite(ctx)
|
2019-06-04 13:31:39 +01:00
|
|
|
if err != nil {
|
2019-08-16 16:33:51 +01:00
|
|
|
if ordersBySatellite == nil {
|
2019-08-22 15:33:14 +01:00
|
|
|
service.log.Error("listing orders", zap.Error(err))
|
2019-08-16 16:33:51 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-08-22 15:33:14 +01:00
|
|
|
service.log.Warn("DB contains invalid marshalled orders", zap.Error(err))
|
2019-06-04 13:31:39 +01:00
|
|
|
}
|
2019-03-21 13:24:26 +00:00
|
|
|
|
2019-07-31 17:40:08 +01:00
|
|
|
requests := make(chan ArchiveRequest, batchSize)
|
|
|
|
var batchGroup errgroup.Group
|
2019-08-22 15:33:14 +01:00
|
|
|
batchGroup.Go(func() error { return service.handleBatches(ctx, requests) })
|
2019-07-31 17:40:08 +01:00
|
|
|
|
2019-06-04 13:31:39 +01:00
|
|
|
if len(ordersBySatellite) > 0 {
|
|
|
|
var group errgroup.Group
|
2019-08-22 15:33:14 +01:00
|
|
|
ctx, cancel := context.WithTimeout(ctx, service.config.SenderTimeout)
|
2019-06-04 13:31:39 +01:00
|
|
|
defer cancel()
|
2019-03-27 10:24:35 +00:00
|
|
|
|
2019-06-04 13:31:39 +01:00
|
|
|
for satelliteID, orders := range ordersBySatellite {
|
|
|
|
satelliteID, orders := satelliteID, orders
|
|
|
|
group.Go(func() error {
|
2019-08-22 15:33:14 +01:00
|
|
|
service.Settle(ctx, satelliteID, orders, requests)
|
2019-06-04 13:31:39 +01:00
|
|
|
return nil
|
|
|
|
})
|
2019-03-21 13:24:26 +00:00
|
|
|
}
|
2019-07-31 17:40:08 +01:00
|
|
|
|
2019-06-04 13:31:39 +01:00
|
|
|
_ = group.Wait() // doesn't return errors
|
|
|
|
} else {
|
2019-08-22 15:33:14 +01:00
|
|
|
service.log.Debug("no orders to send")
|
2019-06-04 13:31:39 +01:00
|
|
|
}
|
2019-03-21 13:24:26 +00:00
|
|
|
|
2019-07-31 17:40:08 +01:00
|
|
|
close(requests)
|
2019-08-22 15:33:14 +01:00
|
|
|
err = batchGroup.Wait()
|
|
|
|
if err != nil {
|
|
|
|
service.log.Error("archiving orders", zap.Error(err))
|
|
|
|
}
|
|
|
|
return nil
|
2019-03-21 13:24:26 +00:00
|
|
|
}
|
|
|
|
|
2019-03-27 10:24:35 +00:00
|
|
|
// Settle uploads orders to the satellite.
|
2019-08-22 15:33:14 +01:00
|
|
|
func (service *Service) Settle(ctx context.Context, satelliteID storj.NodeID, orders []*Info, requests chan ArchiveRequest) {
|
|
|
|
log := service.log.Named(satelliteID.String())
|
|
|
|
err := service.settle(ctx, log, satelliteID, orders, requests)
|
2019-06-04 13:31:39 +01:00
|
|
|
if err != nil {
|
|
|
|
log.Error("failed to settle orders", zap.Error(err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-22 15:33:14 +01:00
|
|
|
func (service *Service) handleBatches(ctx context.Context, requests chan ArchiveRequest) (err error) {
|
2019-07-31 17:40:08 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
// In case anything goes wrong, discard everything from the channel.
|
|
|
|
defer func() {
|
|
|
|
for range requests {
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
buffer := make([]ArchiveRequest, 0, cap(requests))
|
|
|
|
|
2019-08-29 19:22:22 +01:00
|
|
|
archive := func(ctx context.Context, archivedAt time.Time, requests ...ArchiveRequest) error {
|
2019-08-22 15:33:14 +01:00
|
|
|
if err := service.orders.Archive(ctx, time.Now().UTC(), buffer...); err != nil {
|
2019-08-16 15:53:22 +01:00
|
|
|
if !OrderNotFoundError.Has(err) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-08-22 15:33:14 +01:00
|
|
|
service.log.Warn("some unsent order aren't in the DB", zap.Error(err))
|
2019-07-31 17:40:08 +01:00
|
|
|
}
|
2019-08-29 19:22:22 +01:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
for request := range requests {
|
|
|
|
buffer = append(buffer, request)
|
|
|
|
if len(buffer) < cap(buffer) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := archive(ctx, time.Now().UTC(), buffer...); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-07-31 17:40:08 +01:00
|
|
|
buffer = buffer[:0]
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(buffer) > 0 {
|
2019-08-29 19:22:22 +01:00
|
|
|
return archive(ctx, time.Now().UTC(), buffer...)
|
2019-07-31 17:40:08 +01:00
|
|
|
}
|
2019-08-29 19:22:22 +01:00
|
|
|
|
2019-07-31 17:40:08 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-08-22 15:33:14 +01:00
|
|
|
func (service *Service) settle(ctx context.Context, log *zap.Logger, satelliteID storj.NodeID, orders []*Info, requests chan ArchiveRequest) (err error) {
|
2019-06-04 13:31:39 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-03-21 13:24:26 +00:00
|
|
|
|
|
|
|
log.Info("sending", zap.Int("count", len(orders)))
|
|
|
|
defer log.Info("finished")
|
|
|
|
|
2019-08-22 15:33:14 +01:00
|
|
|
address, err := service.trust.GetAddress(ctx, satelliteID)
|
2019-03-21 13:24:26 +00:00
|
|
|
if err != nil {
|
2020-01-14 11:41:12 +00:00
|
|
|
return OrderError.New("unable to get satellite address: %w", err)
|
2019-07-18 18:15:09 +01:00
|
|
|
}
|
2019-03-21 13:24:26 +00:00
|
|
|
|
2019-09-19 05:46:39 +01:00
|
|
|
conn, err := service.dialer.DialAddressID(ctx, address, satelliteID)
|
2019-03-21 13:24:26 +00:00
|
|
|
if err != nil {
|
2020-01-14 11:41:12 +00:00
|
|
|
return OrderError.New("unable to connect to the satellite: %w", err)
|
2019-03-21 13:24:26 +00:00
|
|
|
}
|
2019-09-19 05:46:39 +01:00
|
|
|
defer func() { err = errs.Combine(err, conn.Close()) }()
|
2019-03-21 13:24:26 +00:00
|
|
|
|
2019-12-22 15:07:50 +00:00
|
|
|
stream, err := pb.NewDRPCOrdersClient(conn.Raw()).Settlement(ctx)
|
2019-03-21 13:24:26 +00:00
|
|
|
if err != nil {
|
2020-01-14 11:41:12 +00:00
|
|
|
return OrderError.New("failed to start settlement: %w", err)
|
2019-03-21 13:24:26 +00:00
|
|
|
}
|
|
|
|
|
2019-09-13 13:50:39 +01:00
|
|
|
var group errgroup.Group
|
|
|
|
var sendErrors errs.Group
|
|
|
|
|
2019-03-21 13:24:26 +00:00
|
|
|
group.Go(func() error {
|
|
|
|
for _, order := range orders {
|
2019-08-16 15:53:22 +01:00
|
|
|
req := pb.SettlementRequest{
|
2019-03-21 13:24:26 +00:00
|
|
|
Limit: order.Limit,
|
|
|
|
Order: order.Order,
|
2019-08-16 15:53:22 +01:00
|
|
|
}
|
2019-09-19 05:46:39 +01:00
|
|
|
err := stream.Send(&req)
|
2019-03-21 13:24:26 +00:00
|
|
|
if err != nil {
|
2020-01-14 11:41:12 +00:00
|
|
|
err = OrderError.New("sending settlement agreements returned an error: %w", err)
|
2019-09-19 05:46:39 +01:00
|
|
|
log.Error("rpc client when sending new orders settlements",
|
2019-08-16 15:53:22 +01:00
|
|
|
zap.Error(err),
|
|
|
|
zap.Any("request", req),
|
|
|
|
)
|
2019-09-13 13:50:39 +01:00
|
|
|
sendErrors.Add(err)
|
2019-08-16 15:53:22 +01:00
|
|
|
return nil
|
2019-03-21 13:24:26 +00:00
|
|
|
}
|
|
|
|
}
|
2019-08-16 15:53:22 +01:00
|
|
|
|
2019-09-19 05:46:39 +01:00
|
|
|
err := stream.CloseSend()
|
2019-08-16 15:53:22 +01:00
|
|
|
if err != nil {
|
2020-01-14 11:41:12 +00:00
|
|
|
err = OrderError.New("CloseSend settlement agreements returned an error: %w", err)
|
2019-09-19 05:46:39 +01:00
|
|
|
log.Error("rpc client error when closing sender ", zap.Error(err))
|
2019-09-13 13:50:39 +01:00
|
|
|
sendErrors.Add(err)
|
2019-08-16 15:53:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2019-03-21 13:24:26 +00:00
|
|
|
})
|
|
|
|
|
2019-09-13 13:50:39 +01:00
|
|
|
var errList errs.Group
|
2019-03-21 13:24:26 +00:00
|
|
|
for {
|
2019-09-19 05:46:39 +01:00
|
|
|
response, err := stream.Recv()
|
2019-03-21 13:24:26 +00:00
|
|
|
if err != nil {
|
|
|
|
if err == io.EOF {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2020-01-14 11:41:12 +00:00
|
|
|
err = OrderError.New("failed to receive settlement response: %w", err)
|
2020-01-26 18:45:57 +00:00
|
|
|
log.Error("rpc client error when receiving new order settlements", zap.Error(err))
|
2019-08-16 15:53:22 +01:00
|
|
|
errList.Add(err)
|
2019-03-21 13:24:26 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2019-07-31 17:40:08 +01:00
|
|
|
var status Status
|
2019-03-21 13:24:26 +00:00
|
|
|
switch response.Status {
|
|
|
|
case pb.SettlementResponse_ACCEPTED:
|
2019-07-31 17:40:08 +01:00
|
|
|
status = StatusAccepted
|
2019-03-21 13:24:26 +00:00
|
|
|
case pb.SettlementResponse_REJECTED:
|
2019-07-31 17:40:08 +01:00
|
|
|
status = StatusRejected
|
2019-03-21 13:24:26 +00:00
|
|
|
default:
|
2019-08-16 15:53:22 +01:00
|
|
|
err := OrderError.New("unexpected settlement status response: %d", response.Status)
|
2020-01-26 18:45:57 +00:00
|
|
|
log.Error("rpc client received an unexpected new orders settlement status",
|
2019-08-16 15:53:22 +01:00
|
|
|
zap.Error(err), zap.Any("response", response),
|
|
|
|
)
|
|
|
|
errList.Add(err)
|
2019-07-31 17:40:08 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
requests <- ArchiveRequest{
|
|
|
|
Satellite: satelliteID,
|
|
|
|
Serial: response.SerialNumber,
|
|
|
|
Status: status,
|
2019-03-21 13:24:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-13 13:50:39 +01:00
|
|
|
// errors of this group are reported to sendErrors and it always return nil
|
2019-08-16 15:53:22 +01:00
|
|
|
_ = group.Wait()
|
2019-09-13 13:50:39 +01:00
|
|
|
errList.Add(sendErrors...)
|
|
|
|
|
2019-06-04 13:31:39 +01:00
|
|
|
return errList.Err()
|
2019-03-21 13:24:26 +00:00
|
|
|
}
|
|
|
|
|
2019-12-30 20:35:26 +00:00
|
|
|
// sleep for random interval in [0;maxSleep)
|
|
|
|
// returns error if context was cancelled
|
|
|
|
func (service *Service) sleep(ctx context.Context) error {
|
|
|
|
if service.config.MaxSleep <= 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
jitter := time.Duration(rand.Int63n(int64(service.config.MaxSleep)))
|
|
|
|
if !sync2.Sleep(ctx, jitter) {
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-03-21 13:24:26 +00:00
|
|
|
// Close stops the sending service.
|
2019-08-22 15:33:14 +01:00
|
|
|
func (service *Service) Close() error {
|
|
|
|
service.Sender.Close()
|
|
|
|
service.Cleanup.Close()
|
2019-03-21 13:24:26 +00:00
|
|
|
return nil
|
|
|
|
}
|