github.com/sberex/go-sberex@v1.8.2-0.20181113200658-ed96ac38f7d7/swarm/network/syncdb.go (about) 1 // This file is part of the go-sberex library. The go-sberex library is 2 // free software: you can redistribute it and/or modify it under the terms 3 // of the GNU Lesser General Public License as published by the Free 4 // Software Foundation, either version 3 of the License, or (at your option) 5 // any later version. 6 // 7 // The go-sberex library is distributed in the hope that it will be useful, 8 // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 10 // General Public License <http://www.gnu.org/licenses/> for more details. 11 12 package network 13 14 import ( 15 "encoding/binary" 16 "fmt" 17 18 "github.com/Sberex/go-sberex/log" 19 "github.com/Sberex/go-sberex/swarm/storage" 20 "github.com/syndtr/goleveldb/leveldb" 21 "github.com/syndtr/goleveldb/leveldb/iterator" 22 ) 23 24 const counterKeyPrefix = 0x01 25 26 /* 27 syncDb is a queueing service for outgoing deliveries. 28 One instance per priority queue for each peer 29 30 a syncDb instance maintains an in-memory buffer (of capacity bufferSize) 31 once its in-memory buffer is full it switches to persisting in db 32 and dbRead iterator iterates through the items keeping their order 33 once the db read catches up (there is no more items in the db) then 34 it switches back to in-memory buffer. 35 36 when syncdb is stopped all items in the buffer are saved to the db 37 */ 38 type syncDb struct { 39 start []byte // this syncdb starting index in requestdb 40 key storage.Key // remote peers address key 41 counterKey []byte // db key to persist counter 42 priority uint // priotity High|Medium|Low 43 buffer chan interface{} // incoming request channel 44 db *storage.LDBDatabase // underlying db (TODO should be interface) 45 done chan bool // chan to signal goroutines finished quitting 46 quit chan bool // chan to signal quitting to goroutines 47 total, dbTotal int // counts for one session 48 batch chan chan int // channel for batch requests 49 dbBatchSize uint // number of items before batch is saved 50 } 51 52 // constructor needs a shared request db (leveldb) 53 // priority is used in the index key 54 // uses a buffer and a leveldb for persistent storage 55 // bufferSize, dbBatchSize are config parameters 56 func newSyncDb(db *storage.LDBDatabase, key storage.Key, priority uint, bufferSize, dbBatchSize uint, deliver func(interface{}, chan bool) bool) *syncDb { 57 start := make([]byte, 42) 58 start[1] = byte(priorities - priority) 59 copy(start[2:34], key) 60 61 counterKey := make([]byte, 34) 62 counterKey[0] = counterKeyPrefix 63 copy(counterKey[1:], start[1:34]) 64 65 syncdb := &syncDb{ 66 start: start, 67 key: key, 68 counterKey: counterKey, 69 priority: priority, 70 buffer: make(chan interface{}, bufferSize), 71 db: db, 72 done: make(chan bool), 73 quit: make(chan bool), 74 batch: make(chan chan int), 75 dbBatchSize: dbBatchSize, 76 } 77 log.Trace(fmt.Sprintf("syncDb[peer: %v, priority: %v] - initialised", key.Log(), priority)) 78 79 // starts the main forever loop reading from buffer 80 go syncdb.bufferRead(deliver) 81 return syncdb 82 } 83 84 /* 85 bufferRead is a forever iterator loop that takes care of delivering 86 outgoing store requests reads from incoming buffer 87 88 its argument is the deliver function taking the item as first argument 89 and a quit channel as second. 90 Closing of this channel is supposed to abort all waiting for delivery 91 (typically network write) 92 93 The iteration switches between 2 modes, 94 * buffer mode reads the in-memory buffer and delivers the items directly 95 * db mode reads from the buffer and writes to the db, parallelly another 96 routine is started that reads from the db and delivers items 97 98 If there is buffer contention in buffer mode (slow network, high upload volume) 99 syncdb switches to db mode and starts dbRead 100 Once db backlog is delivered, it reverts back to in-memory buffer 101 102 It is automatically started when syncdb is initialised. 103 104 It saves the buffer to db upon receiving quit signal. syncDb#stop() 105 */ 106 func (self *syncDb) bufferRead(deliver func(interface{}, chan bool) bool) { 107 var buffer, db chan interface{} // channels representing the two read modes 108 var more bool 109 var req interface{} 110 var entry *syncDbEntry 111 var inBatch, inDb int 112 batch := new(leveldb.Batch) 113 var dbSize chan int 114 quit := self.quit 115 counterValue := make([]byte, 8) 116 117 // counter is used for keeping the items in order, persisted to db 118 // start counter where db was at, 0 if not found 119 data, err := self.db.Get(self.counterKey) 120 var counter uint64 121 if err == nil { 122 counter = binary.BigEndian.Uint64(data) 123 log.Trace(fmt.Sprintf("syncDb[%v/%v] - counter read from db at %v", self.key.Log(), self.priority, counter)) 124 } else { 125 log.Trace(fmt.Sprintf("syncDb[%v/%v] - counter starts at %v", self.key.Log(), self.priority, counter)) 126 } 127 128 LOOP: 129 for { 130 // waiting for item next in the buffer, or quit signal or batch request 131 select { 132 // buffer only closes when writing to db 133 case req = <-buffer: 134 // deliver request : this is blocking on network write so 135 // it is passed the quit channel as argument, so that it returns 136 // if syncdb is stopped. In this case we need to save the item to the db 137 more = deliver(req, self.quit) 138 if !more { 139 log.Debug(fmt.Sprintf("syncDb[%v/%v] quit: switching to db. session tally (db/total): %v/%v", self.key.Log(), self.priority, self.dbTotal, self.total)) 140 // received quit signal, save request currently waiting delivery 141 // by switching to db mode and closing the buffer 142 buffer = nil 143 db = self.buffer 144 close(db) 145 quit = nil // needs to block the quit case in select 146 break // break from select, this item will be written to the db 147 } 148 self.total++ 149 log.Trace(fmt.Sprintf("syncDb[%v/%v] deliver (db/total): %v/%v", self.key.Log(), self.priority, self.dbTotal, self.total)) 150 // by the time deliver returns, there were new writes to the buffer 151 // if buffer contention is detected, switch to db mode which drains 152 // the buffer so no process will block on pushing store requests 153 if len(buffer) == cap(buffer) { 154 log.Debug(fmt.Sprintf("syncDb[%v/%v] buffer full %v: switching to db. session tally (db/total): %v/%v", self.key.Log(), self.priority, cap(buffer), self.dbTotal, self.total)) 155 buffer = nil 156 db = self.buffer 157 } 158 continue LOOP 159 160 // incoming entry to put into db 161 case req, more = <-db: 162 if !more { 163 // only if quit is called, saved all the buffer 164 binary.BigEndian.PutUint64(counterValue, counter) 165 batch.Put(self.counterKey, counterValue) // persist counter in batch 166 self.writeSyncBatch(batch) // save batch 167 log.Trace(fmt.Sprintf("syncDb[%v/%v] quitting: save current batch to db", self.key.Log(), self.priority)) 168 break LOOP 169 } 170 self.dbTotal++ 171 self.total++ 172 // otherwise break after select 173 case dbSize = <-self.batch: 174 // explicit request for batch 175 if inBatch == 0 && quit != nil { 176 // there was no writes since the last batch so db depleted 177 // switch to buffer mode 178 log.Debug(fmt.Sprintf("syncDb[%v/%v] empty db: switching to buffer", self.key.Log(), self.priority)) 179 db = nil 180 buffer = self.buffer 181 dbSize <- 0 // indicates to 'caller' that batch has been written 182 inDb = 0 183 continue LOOP 184 } 185 binary.BigEndian.PutUint64(counterValue, counter) 186 batch.Put(self.counterKey, counterValue) 187 log.Debug(fmt.Sprintf("syncDb[%v/%v] write batch %v/%v - %x - %x", self.key.Log(), self.priority, inBatch, counter, self.counterKey, counterValue)) 188 batch = self.writeSyncBatch(batch) 189 dbSize <- inBatch // indicates to 'caller' that batch has been written 190 inBatch = 0 191 continue LOOP 192 193 // closing syncDb#quit channel is used to signal to all goroutines to quit 194 case <-quit: 195 // need to save backlog, so switch to db mode 196 db = self.buffer 197 buffer = nil 198 quit = nil 199 log.Trace(fmt.Sprintf("syncDb[%v/%v] quitting: save buffer to db", self.key.Log(), self.priority)) 200 close(db) 201 continue LOOP 202 } 203 204 // only get here if we put req into db 205 entry, err = self.newSyncDbEntry(req, counter) 206 if err != nil { 207 log.Warn(fmt.Sprintf("syncDb[%v/%v] saving request %v (#%v/%v) failed: %v", self.key.Log(), self.priority, req, inBatch, inDb, err)) 208 continue LOOP 209 } 210 batch.Put(entry.key, entry.val) 211 log.Trace(fmt.Sprintf("syncDb[%v/%v] to batch %v '%v' (#%v/%v/%v)", self.key.Log(), self.priority, req, entry, inBatch, inDb, counter)) 212 // if just switched to db mode and not quitting, then launch dbRead 213 // in a parallel go routine to send deliveries from db 214 if inDb == 0 && quit != nil { 215 log.Trace(fmt.Sprintf("syncDb[%v/%v] start dbRead", self.key.Log(), self.priority)) 216 go self.dbRead(true, counter, deliver) 217 } 218 inDb++ 219 inBatch++ 220 counter++ 221 // need to save the batch if it gets too large (== dbBatchSize) 222 if inBatch%int(self.dbBatchSize) == 0 { 223 batch = self.writeSyncBatch(batch) 224 } 225 } 226 log.Info(fmt.Sprintf("syncDb[%v:%v]: saved %v keys (saved counter at %v)", self.key.Log(), self.priority, inBatch, counter)) 227 close(self.done) 228 } 229 230 // writes the batch to the db and returns a new batch object 231 func (self *syncDb) writeSyncBatch(batch *leveldb.Batch) *leveldb.Batch { 232 err := self.db.Write(batch) 233 if err != nil { 234 log.Warn(fmt.Sprintf("syncDb[%v/%v] saving batch to db failed: %v", self.key.Log(), self.priority, err)) 235 return batch 236 } 237 return new(leveldb.Batch) 238 } 239 240 // abstract type for db entries (TODO could be a feature of Receipts) 241 type syncDbEntry struct { 242 key, val []byte 243 } 244 245 func (self syncDbEntry) String() string { 246 return fmt.Sprintf("key: %x, value: %x", self.key, self.val) 247 } 248 249 /* 250 dbRead is iterating over store requests to be sent over to the peer 251 this is mainly to prevent crashes due to network output buffer contention (???) 252 as well as to make syncronisation resilient to disconnects 253 the messages are supposed to be sent in the p2p priority queue. 254 255 the request DB is shared between peers, but domains for each syncdb 256 are disjoint. dbkeys (42 bytes) are structured: 257 * 0: 0x00 (0x01 reserved for counter key) 258 * 1: priorities - priority (so that high priority can be replayed first) 259 * 2-33: peers address 260 * 34-41: syncdb counter to preserve order (this field is missing for the counter key) 261 262 values (40 bytes) are: 263 * 0-31: key 264 * 32-39: request id 265 266 dbRead needs a boolean to indicate if on first round all the historical 267 record is synced. Second argument to indicate current db counter 268 The third is the function to apply 269 */ 270 func (self *syncDb) dbRead(useBatches bool, counter uint64, fun func(interface{}, chan bool) bool) { 271 key := make([]byte, 42) 272 copy(key, self.start) 273 binary.BigEndian.PutUint64(key[34:], counter) 274 var batches, n, cnt, total int 275 var more bool 276 var entry *syncDbEntry 277 var it iterator.Iterator 278 var del *leveldb.Batch 279 batchSizes := make(chan int) 280 281 for { 282 // if useBatches is false, cnt is not set 283 if useBatches { 284 // this could be called before all cnt items sent out 285 // so that loop is not blocking while delivering 286 // only relevant if cnt is large 287 select { 288 case self.batch <- batchSizes: 289 case <-self.quit: 290 return 291 } 292 // wait for the write to finish and get the item count in the next batch 293 cnt = <-batchSizes 294 batches++ 295 if cnt == 0 { 296 // empty 297 return 298 } 299 } 300 it = self.db.NewIterator() 301 it.Seek(key) 302 if !it.Valid() { 303 copy(key, self.start) 304 useBatches = true 305 continue 306 } 307 del = new(leveldb.Batch) 308 log.Trace(fmt.Sprintf("syncDb[%v/%v]: new iterator: %x (batch %v, count %v)", self.key.Log(), self.priority, key, batches, cnt)) 309 310 for n = 0; !useBatches || n < cnt; it.Next() { 311 copy(key, it.Key()) 312 if len(key) == 0 || key[0] != 0 { 313 copy(key, self.start) 314 useBatches = true 315 break 316 } 317 val := make([]byte, 40) 318 copy(val, it.Value()) 319 entry = &syncDbEntry{key, val} 320 // log.Trace(fmt.Sprintf("syncDb[%v/%v] - %v, batches: %v, total: %v, session total from db: %v/%v", self.key.Log(), self.priority, self.key.Log(), batches, total, self.dbTotal, self.total)) 321 more = fun(entry, self.quit) 322 if !more { 323 // quit received when waiting to deliver entry, the entry will not be deleted 324 log.Trace(fmt.Sprintf("syncDb[%v/%v] batch %v quit after %v/%v items", self.key.Log(), self.priority, batches, n, cnt)) 325 break 326 } 327 // since subsequent batches of the same db session are indexed incrementally 328 // deleting earlier batches can be delayed and parallelised 329 // this could be batch delete when db is idle (but added complexity esp when quitting) 330 del.Delete(key) 331 n++ 332 total++ 333 } 334 log.Debug(fmt.Sprintf("syncDb[%v/%v] - db session closed, batches: %v, total: %v, session total from db: %v/%v", self.key.Log(), self.priority, batches, total, self.dbTotal, self.total)) 335 self.db.Write(del) // this could be async called only when db is idle 336 it.Release() 337 } 338 } 339 340 // 341 func (self *syncDb) stop() { 342 close(self.quit) 343 <-self.done 344 } 345 346 // calculate a dbkey for the request, for the db to work 347 // see syncdb for db key structure 348 // polimorphic: accepted types, see syncer#addRequest 349 func (self *syncDb) newSyncDbEntry(req interface{}, counter uint64) (entry *syncDbEntry, err error) { 350 var key storage.Key 351 var chunk *storage.Chunk 352 var id uint64 353 var ok bool 354 var sreq *storeRequestMsgData 355 356 if key, ok = req.(storage.Key); ok { 357 id = generateId() 358 } else if chunk, ok = req.(*storage.Chunk); ok { 359 key = chunk.Key 360 id = generateId() 361 } else if sreq, ok = req.(*storeRequestMsgData); ok { 362 key = sreq.Key 363 id = sreq.Id 364 } else if entry, ok = req.(*syncDbEntry); !ok { 365 return nil, fmt.Errorf("type not allowed: %v (%T)", req, req) 366 } 367 368 // order by peer > priority > seqid 369 // value is request id if exists 370 if entry == nil { 371 dbkey := make([]byte, 42) 372 dbval := make([]byte, 40) 373 374 // encode key 375 copy(dbkey[:], self.start[:34]) // db peer 376 binary.BigEndian.PutUint64(dbkey[34:], counter) 377 // encode value 378 copy(dbval, key[:]) 379 binary.BigEndian.PutUint64(dbval[32:], id) 380 381 entry = &syncDbEntry{dbkey, dbval} 382 } 383 return 384 }