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

     1  package accounting
     2  
     3  import (
     4  	"encoding/json"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"time"
     9  
    10  	"gitlab.com/NebulousLabs/errors"
    11  	"gitlab.com/SkynetLabs/skyd/build"
    12  	"gitlab.com/SkynetLabs/skyd/skymodules"
    13  	"go.sia.tech/siad/persist"
    14  	"go.sia.tech/siad/types"
    15  )
    16  
    17  const (
    18  	// logFile is the name of the log file for the Accounting module.
    19  	logFile string = skymodules.AccountingDir + ".log"
    20  
    21  	// persistFile is the name of the persist file
    22  	persistFile string = "accounting"
    23  )
    24  
    25  var (
    26  	// metadataHeader is the header of the metadata for the persist file
    27  	metadataHeader = types.NewSpecifier("Accounting\n")
    28  
    29  	// persistErrorInterval is the interval at which the persist loop will wait in
    30  	// the event of an error.
    31  	persistErrorInterval = build.Select(build.Var{
    32  		Dev:      time.Second,
    33  		Standard: time.Minute,
    34  		Testing:  time.Millisecond * 100,
    35  	}).(time.Duration)
    36  
    37  	// persistInterval is the interval at which the accounting information will be
    38  	// persisted.
    39  	persistInterval = build.Select(build.Var{
    40  		Dev:      time.Minute,
    41  		Standard: time.Hour * 24,
    42  		Testing:  time.Second,
    43  	}).(time.Duration)
    44  )
    45  
    46  // callThreadedPersistAccounting is a background loop that persists the
    47  // accounting information based on the persistInterval.
    48  func (a *Accounting) callThreadedPersistAccounting() {
    49  	err := a.staticTG.Add()
    50  	if err != nil {
    51  		return
    52  	}
    53  	defer a.staticTG.Done()
    54  
    55  	// Determine the initial interval for persisting the accounting information
    56  	a.mu.Lock()
    57  	var lastPersistTime time.Time
    58  	if len(a.history) > 0 {
    59  		lastPersistTime = time.Unix(a.history[len(a.history)-1].Timestamp, 0)
    60  	}
    61  	a.mu.Unlock()
    62  	interval := persistInterval - time.Since(lastPersistTime)
    63  	if interval <= 0 {
    64  		// If it has been longer than the persistInterval then set the interval to
    65  		// 0 so that we persist immediately
    66  		interval = 0
    67  	}
    68  
    69  	// Persist the accounting information in a loop until there is a shutdown
    70  	// event.
    71  	for {
    72  		select {
    73  		case <-a.staticTG.StopChan():
    74  			return
    75  		case <-time.After(interval):
    76  		}
    77  		err = a.managedUpdateAndPersistAccounting()
    78  		if err != nil {
    79  			a.staticLog.Println("WARN: Persist loop error:", err)
    80  			interval = persistErrorInterval
    81  		} else {
    82  			interval = persistInterval
    83  		}
    84  	}
    85  }
    86  
    87  // initPersist initializes the persistence for the Accounting module
    88  func (a *Accounting) initPersist() error {
    89  	// Make sure the persistence directory exists
    90  	err := os.MkdirAll(a.staticPersistDir, skymodules.DefaultDirPerm)
    91  	if err != nil {
    92  		return errors.AddContext(err, "unable to create persistence directory")
    93  	}
    94  
    95  	// Initialize the log
    96  	a.staticLog, err = persist.NewFileLogger(filepath.Join(a.staticPersistDir, logFile))
    97  	if err != nil {
    98  		return errors.AddContext(err, "unable to initialize the accounting log")
    99  	}
   100  	err = a.staticTG.AfterStop(a.staticLog.Close)
   101  	if err != nil {
   102  		return errors.AddContext(err, "unable to add log close to threadgroup AfterStop")
   103  	}
   104  
   105  	// Initialize the AOP
   106  	var reader io.Reader
   107  	a.staticAOP, reader, err = persist.NewAppendOnlyPersist(a.staticPersistDir, persistFile, metadataHeader, persist.MetadataVersionv156)
   108  	if err != nil {
   109  		return errors.AddContext(err, "unable to create AppendOnlyPersist")
   110  	}
   111  	err = a.staticTG.AfterStop(a.staticAOP.Close)
   112  	if err != nil {
   113  		return errors.AddContext(err, "unable to add AOP close to threadgroup AfterStop")
   114  	}
   115  
   116  	// Unmarshal the persistence
   117  	persistence, err := unmarshalPersistence(reader)
   118  	if err != nil {
   119  		return errors.AddContext(err, "unable to unmarshal persistence")
   120  	}
   121  
   122  	// Load persistence into memory
   123  	a.history = persistence
   124  	return nil
   125  }
   126  
   127  // managedUpdateAndPersistAccounting will update the accounting information and write the
   128  // information to disk.
   129  func (a *Accounting) managedUpdateAndPersistAccounting() error {
   130  	logStr := "Update and Persist error"
   131  	// Update the persistence information
   132  	ai, err := a.callUpdateAccounting()
   133  	if err != nil {
   134  		err = errors.AddContext(err, "unable to update accounting information")
   135  		a.staticLog.Printf("WARN: %v:%v", logStr, err)
   136  		return err
   137  	}
   138  
   139  	// Marshall the persistence
   140  	data, err := marshalPersistence(ai)
   141  	if err != nil {
   142  		err = errors.AddContext(err, "unable to marshal persistence")
   143  		a.staticLog.Printf("WARN: %v:%v", logStr, err)
   144  		return err
   145  	}
   146  
   147  	// Persist
   148  	_, err = a.staticAOP.Write(data)
   149  	if err != nil {
   150  		err = errors.AddContext(err, "unable to write persistence to disk")
   151  		a.staticLog.Printf("WARN: %v:%v", logStr, err)
   152  		return err
   153  	}
   154  
   155  	// Add to the history
   156  	a.mu.Lock()
   157  	a.history = append(a.history, ai)
   158  	a.mu.Unlock()
   159  
   160  	return nil
   161  }
   162  
   163  // marshalPersistence marshals the persistence.
   164  func marshalPersistence(ai skymodules.AccountingInfo) ([]byte, error) {
   165  	// Marshal the persistence
   166  	data, err := json.Marshal(ai)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	return data, nil
   171  }
   172  
   173  // unmarshalPersistence uses a json Decoder to read the persisted json entries
   174  // and unmarshals them.
   175  func unmarshalPersistence(r io.Reader) ([]skymodules.AccountingInfo, error) {
   176  	// Create decoder
   177  	d := json.NewDecoder(r)
   178  
   179  	var ais []skymodules.AccountingInfo
   180  	for {
   181  		// Decode persisted json entry
   182  		var ai skymodules.AccountingInfo
   183  		err := d.Decode(&ai)
   184  		if errors.Contains(err, io.EOF) {
   185  			break
   186  		}
   187  		if err != nil {
   188  			return nil, errors.AddContext(err, "unable to read from reader")
   189  		}
   190  		// Append entry
   191  		ais = append(ais, ai)
   192  	}
   193  	return ais, nil
   194  }