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 }