diff --git a/cmd.go b/cmd.go index 2698992..4a16488 100644 --- a/cmd.go +++ b/cmd.go @@ -107,10 +107,16 @@ func (s *BtcWalletStore) Rollback(height int64, hash *btcwire.ShaHash) { func (w *BtcWallet) Rollback(height int64, hash *btcwire.ShaHash) { w.UtxoStore.Lock() w.UtxoStore.dirty = w.UtxoStore.dirty || w.UtxoStore.s.Rollback(height, hash) + if w.UtxoStore.dirty { + AddDirtyAccount(w) + } w.UtxoStore.Unlock() w.TxStore.Lock() w.TxStore.dirty = w.TxStore.dirty || w.TxStore.s.Rollback(height, hash) + if w.TxStore.dirty { + AddDirtyAccount(w) + } w.TxStore.Unlock() } @@ -449,6 +455,7 @@ func (w *BtcWallet) newBlockTxHandler(result interface{}, e *btcjson.Error) bool txs := w.TxStore.s w.TxStore.s = append(txs, t) w.TxStore.dirty = true + AddDirtyAccount(w) w.TxStore.Unlock() }() @@ -469,6 +476,7 @@ func (w *BtcWallet) newBlockTxHandler(result interface{}, e *btcjson.Error) bool w.UtxoStore.Lock() w.UtxoStore.s = append(w.UtxoStore.s, u) w.UtxoStore.dirty = true + AddDirtyAccount(w) w.UtxoStore.Unlock() confirmed := w.CalculateBalance(6) unconfirmed := w.CalculateBalance(0) - confirmed @@ -505,7 +513,7 @@ func main() { } cfg = tcfg - // Open wallet + // Open default wallet w, err := OpenWallet(cfg, "") if err != nil { log.Info(err.Error()) @@ -530,6 +538,9 @@ func main() { // Begin generating new IDs for JSON calls. go JSONIDGenerator(NewJSONID) + // Begin wallet to disk syncer. + go DirtyAccountUpdater() + for { replies := make(chan error) done := make(chan int) diff --git a/cmdmgr.go b/cmdmgr.go index 4302bbb..82f4cea 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -493,6 +493,7 @@ func SendFrom(reply chan []byte, msg *btcjson.Message) { modified := w.UtxoStore.s.Remove(inputs) if modified { w.UtxoStore.dirty = true + AddDirtyAccount(w) w.UtxoStore.Unlock() // Notify all frontends of new account balances. @@ -637,6 +638,7 @@ func SendMany(reply chan []byte, msg *btcjson.Message) { modified := w.UtxoStore.s.Remove(inputs) if modified { w.UtxoStore.dirty = true + AddDirtyAccount(w) w.UtxoStore.Unlock() // Notify all frontends of new account balances. @@ -768,6 +770,7 @@ func CreateEncryptedWallet(reply chan []byte, msg *btcjson.Message) { bw := &BtcWallet{ Wallet: w, name: wname, + dirty: true, NewBlockTxSeqN: n, } // TODO(jrick): only begin tracking wallet if btcwallet is already @@ -775,6 +778,7 @@ func CreateEncryptedWallet(reply chan []byte, msg *btcjson.Message) { bw.Track() wallets.m[wname] = bw + AddDirtyAccount(bw) ReplySuccess(reply, msg.Id, nil) } diff --git a/disksync.go b/disksync.go new file mode 100644 index 0000000..798dc4b --- /dev/null +++ b/disksync.go @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2013 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package main + +import ( + "fmt" + "os" + "path/filepath" + "time" +) + +var dirtyAccountSet = make(map[*BtcWallet]bool) +var addDirtyAccount = make(chan *BtcWallet) + +// DirtyAccountUpdater is responsible for listening for listens for new +// dirty wallets (changed in memory with updaets not yet saved to disk) +// to add to dirtyAccountSet. This is designed to run as a single goroutine. +func DirtyAccountUpdater() { + timer := time.Tick(time.Minute) + for { + select { + case w := <-addDirtyAccount: + dirtyAccountSet[w] = true + + case <-timer: + for w := range dirtyAccountSet { + if err := w.writeDirtyToDisk(); err != nil { + log.Errorf("cannot sync dirty wallet '%v': %v", w.name, err) + } else { + delete(dirtyAccountSet, w) + log.Infof("removed dirty wallet '%v'", w.name) + } + } + } + } +} + +// AddDirtyAccount adds w to a set of items to be synced to disk. The +// dirty flag must still be set on the various dirty elements of the +// account (wallet, transactions, and/or utxos) or nothing will be +// written to disk during the next scheduled sync. +func AddDirtyAccount(w *BtcWallet) { + addDirtyAccount <- w +} + +// writeDirtyToDisk checks for the dirty flag on an account's wallet, +// txstore, and utxostore, writing them to disk if any are dirty. +func (w *BtcWallet) writeDirtyToDisk() error { + // Temporary files append the current time to the normal file name. + // In caes of failure, the most recent temporary file can be inspected + // for validity, and moved to replace the main file. + timeStr := fmt.Sprintf("%v", time.Now().Unix()) + + wdir := walletdir(cfg, w.name) + wfilepath := filepath.Join(wdir, "wallet.bin") + txfilepath := filepath.Join(wdir, "tx.bin") + utxofilepath := filepath.Join(wdir, "utxo.bin") + + // Wallet + if w.dirty { + w.mtx.RLock() + defer w.mtx.RUnlock() + tmpfilepath := wfilepath + "-" + timeStr + tmpfile, err := os.Create(tmpfilepath) + if err != nil { + return err + } + if _, err = w.WriteTo(tmpfile); err != nil { + return err + } + + // TODO(jrick): this should be atomic on *nix, but is not on + // Windows. Use _windows.go to provide atomic renames. + if err = os.Rename(tmpfilepath, wfilepath); err != nil { + return err + } + } + + // Transactions + if w.TxStore.dirty { + w.TxStore.RLock() + defer w.TxStore.RUnlock() + tmpfilepath := txfilepath + "-" + timeStr + tmpfile, err := os.Create(tmpfilepath) + if err != nil { + return err + } + if _, err = w.TxStore.s.WriteTo(tmpfile); err != nil { + return err + } + + // TODO(jrick): this should be atomic on *nix, but is not on + // Windows. Use _windows.go to provide atomic renames. + if err = os.Rename(tmpfilepath, txfilepath); err != nil { + return err + } + } + + // UTXOs + if w.UtxoStore.dirty { + w.UtxoStore.RLock() + defer w.UtxoStore.RUnlock() + tmpfilepath := utxofilepath + "-" + timeStr + tmpfile, err := os.Create(tmpfilepath) + if err != nil { + return err + } + if _, err = w.UtxoStore.s.WriteTo(tmpfile); err != nil { + return err + } + + // TODO(jrick): this should be atomic on *nix, but is not on + // Windows. Use _windows.go to provide atomic renames. + if err = os.Rename(tmpfilepath, utxofilepath); err != nil { + return err + } + } + + return nil +}