gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/accounting/accounting.go (about)

     1  package accounting
     2  
     3  import (
     4  	"math"
     5  	"sort"
     6  	"sync"
     7  	"time"
     8  
     9  	"gitlab.com/NebulousLabs/errors"
    10  	"gitlab.com/NebulousLabs/threadgroup"
    11  	"gitlab.com/SkynetLabs/skyd/build"
    12  	"gitlab.com/SkynetLabs/skyd/skymodules"
    13  	"go.sia.tech/siad/modules"
    14  	"go.sia.tech/siad/persist"
    15  )
    16  
    17  const (
    18  	// DefaultEndRangeTime is the default end time used when one isn't provided
    19  	// by the user.
    20  	DefaultEndRangeTime = math.MaxInt64
    21  )
    22  
    23  var (
    24  	// ErrInvalidRange is the error returned if the end time is before the start
    25  	// time.
    26  	ErrInvalidRange = errors.New("invalid range, end must be after start")
    27  
    28  	// errNilDeps is the error returned when no dependencies are provided
    29  	errNilDeps = errors.New("dependencies cannot be nil")
    30  
    31  	// errNilPersistDir is the error returned when no persistDir is provided
    32  	errNilPersistDir = errors.New("persistDir cannot by blank")
    33  
    34  	// errNilWallet is the error returned when the wallet is nil
    35  	errNilWallet = errors.New("wallet cannot be nil")
    36  
    37  	// errNoEntriesFound is the error returned when no entries are found for
    38  	// a given range
    39  	errNoEntriesFound = errors.New("no entries found for given range")
    40  )
    41  
    42  // Accounting contains the information needed for providing accounting
    43  // information about a Sia node.
    44  type Accounting struct {
    45  	// Modules whose accounting information is tracked
    46  	staticHost   modules.Host
    47  	staticMiner  modules.Miner
    48  	staticRenter skymodules.Renter
    49  	staticWallet modules.Wallet
    50  
    51  	// history is the entire persisted history of the accounting information
    52  	//
    53  	// NOTE: We only persist a small amount of data daily so this is OK and we are
    54  	// not concerned with this struct taking up much memory.
    55  	history []skymodules.AccountingInfo
    56  
    57  	// staticPersistDir is the accounting persist location on disk
    58  	staticPersistDir string
    59  
    60  	// Utilities
    61  	staticAOP  *persist.AppendOnlyPersist
    62  	staticDeps modules.Dependencies
    63  	staticLog  *persist.Logger
    64  	staticTG   threadgroup.ThreadGroup
    65  
    66  	mu sync.Mutex
    67  }
    68  
    69  // NewCustomAccounting initializes the accounting module with custom
    70  // dependencies
    71  func NewCustomAccounting(h modules.Host, m modules.Miner, r skymodules.Renter, w modules.Wallet, persistDir string, deps modules.Dependencies) (*Accounting, error) {
    72  	// Check that at least the wallet is not nil
    73  	if w == nil {
    74  		return nil, errNilWallet
    75  	}
    76  
    77  	// Check required parameters
    78  	if persistDir == "" {
    79  		return nil, errNilPersistDir
    80  	}
    81  	if deps == nil {
    82  		return nil, errNilDeps
    83  	}
    84  
    85  	// Initialize the accounting
    86  	a := &Accounting{
    87  		staticHost:   h,
    88  		staticMiner:  m,
    89  		staticRenter: r,
    90  		staticWallet: w,
    91  
    92  		staticPersistDir: persistDir,
    93  
    94  		staticDeps: deps,
    95  	}
    96  
    97  	// Initialize the persistence
    98  	err := a.initPersist()
    99  	if err != nil {
   100  		return nil, errors.AddContext(err, "unable to initialize the persistence")
   101  	}
   102  
   103  	// Launch background thread to persist the accounting information
   104  	if !a.staticDeps.Disrupt("DisablePersistLoop") {
   105  		go a.callThreadedPersistAccounting()
   106  	}
   107  	return a, nil
   108  }
   109  
   110  // Accounting returns the current accounting information
   111  func (a *Accounting) Accounting(start, end int64) ([]skymodules.AccountingInfo, error) {
   112  	err := a.staticTG.Add()
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	defer a.staticTG.Done()
   117  
   118  	// Check start and end range
   119  	if end < start {
   120  		return nil, ErrInvalidRange
   121  	}
   122  
   123  	// Update the accounting information
   124  	ai, err := a.callUpdateAccounting()
   125  	if err != nil {
   126  		return nil, errors.AddContext(err, "unable to update the accounting information")
   127  	}
   128  
   129  	// Return the requested range
   130  	a.mu.Lock()
   131  	history := append(a.history, ai)
   132  	a.mu.Unlock()
   133  	ais := accountingRange(history, start, end)
   134  	if len(ais) == 0 {
   135  		return nil, errNoEntriesFound
   136  	}
   137  	return ais, nil
   138  }
   139  
   140  // Close closes the accounting module
   141  //
   142  // NOTE: It will not call close on any of the modules it is tracking. Those
   143  // modules are responsible for closing themselves independently.
   144  func (a *Accounting) Close() error {
   145  	return a.staticTG.Stop()
   146  }
   147  
   148  // accountingRange returns a range of accounting information from a provided
   149  // history
   150  func accountingRange(history []skymodules.AccountingInfo, start, end int64) []skymodules.AccountingInfo {
   151  	// Sanity check
   152  	if end < start {
   153  		build.Critical(ErrInvalidRange)
   154  		return nil
   155  	}
   156  
   157  	// Find Start and end indexes
   158  	startIndex := sort.Search(len(history), func(i int) bool {
   159  		return history[i].Timestamp >= start
   160  	})
   161  	endIndex := sort.Search(len(history), func(i int) bool {
   162  		return history[i].Timestamp > end
   163  	})
   164  
   165  	// Return range
   166  	if endIndex == len(history) {
   167  		return history[startIndex:]
   168  	}
   169  	return history[startIndex:endIndex]
   170  }
   171  
   172  // callUpdateAccounting updates the accounting information
   173  func (a *Accounting) callUpdateAccounting() (skymodules.AccountingInfo, error) {
   174  	var ai skymodules.AccountingInfo
   175  	ai.Timestamp = time.Now().Unix()
   176  
   177  	// Get Renter information
   178  	//
   179  	// NOTE: renter is optional so can be nil
   180  	var renterErr error
   181  	if a.staticRenter != nil {
   182  		var metrics skymodules.FinancialMetrics
   183  		metrics, renterErr = a.staticRenter.PeriodSpending()
   184  		if renterErr == nil {
   185  			spending := metrics.ContractorSpending
   186  			_, _, unspentUnallocated := spending.SpendingBreakdown()
   187  			ai.Renter.UnspentUnallocated = unspentUnallocated
   188  			ai.Renter.WithheldFunds = spending.WithheldFunds
   189  		}
   190  	}
   191  
   192  	// Get Wallet information
   193  	sc, sf, _, walletErr := a.staticWallet.ConfirmedBalance()
   194  	if walletErr == nil {
   195  		ai.Wallet.ConfirmedSiacoinBalance = sc
   196  		ai.Wallet.ConfirmedSiafundBalance = sf
   197  	}
   198  
   199  	return ai, errors.Compose(renterErr, walletErr)
   200  }
   201  
   202  // Enforce that Accounting satisfies the skymodules.Accounting interface.
   203  var _ skymodules.Accounting = (*Accounting)(nil)