-
Notifications
You must be signed in to change notification settings - Fork 110
LocalStore #1032
LocalStore #1032
Changes from 71 commits
d8acb12
9ec535a
37205de
b1ded5a
572f3cb
cbb510b
c7beb22
391faa7
58f3f86
2d928bf
4d58a6f
af1b137
96409ff
b782bfe
35376d8
e6a7196
58c7f11
6e8b2ad
7b8510e
cf3ec30
d58e1ee
f2299f4
e6bdda7
b6f5b7a
5732c38
ac9d153
e6e29f5
750268d
a486a09
e17fec2
5605323
5ebbcb5
80c269b
bca85ef
cee4004
b5a0fd2
4fca004
5ad0c8d
cb8078e
c9f5130
da9bab4
d54d7ae
67473be
2299147
5488a2b
38bdf7f
a74eae2
534009f
5edd22d
95726d7
c5dcae3
f3380ea
1d2d470
c26c979
c188281
c5e4c61
015d977
abbb4a6
fb0a822
c423060
c5a6456
33726a4
25a068a
ca1e24f
c62fee3
5ddc75f
87bbd61
6ad67d7
a550388
1dae999
eda338a
ad5b329
8d15e82
3948044
6c8208a
6accc6b
85cd349
7fa1ba9
ebecd05
f056e86
40432d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
// Copyright 2019 The go-ethereum Authors | ||
// This file is part of the go-ethereum library. | ||
// | ||
// The go-ethereum library is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU Lesser General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// The go-ethereum library is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU Lesser General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU Lesser General Public License | ||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
/* | ||
Package localstore provides disk storage layer for Swarm Chunk persistence. | ||
It uses swarm/shed abstractions on top of github.com/syndtr/goleveldb LevelDB | ||
implementation. | ||
|
||
The main type is DB which manages the storage by providing methods to | ||
access and add Chunks and to manage their status. | ||
|
||
Modes are abstractions that do specific changes to Chunks. There are three | ||
mode types: | ||
|
||
- ModeGet, for Chunk access | ||
- ModePut, for adding Chunks to the database | ||
- ModeSet, for changing Chunk statuses | ||
|
||
Every mode type has a corresponding type (Getter, Putter and Setter) | ||
that provides adequate method to perform the opperation and that type | ||
should be injected into localstore consumers instead the whole DB. | ||
This provides more clear insight which operations consumer is performing | ||
on the database. | ||
|
||
Getters, Putters and Setters accept different get, put and set modes | ||
to perform different actions. For example, ModeGet has two different | ||
variables ModeGetRequest and ModeGetSync and dwo different Getters | ||
can be constructed with them that are used when the chunk is requested | ||
or when the chunk is synced as this two events are differently changing | ||
the database. | ||
|
||
Subscription methods are implemented for a specific purpose of | ||
continuous iterations over Chunks that should be provided to | ||
Push and Pull syncing. | ||
|
||
DB implements an internal garbage collector that removes only synced | ||
Chunks from the database based on their most recent access time. | ||
|
||
Internally, DB stores Chunk data and any required information, such as | ||
store and access timestamps in different shed indexes that can be | ||
iterated on by garbage collector or subscriptions. | ||
*/ | ||
package localstore |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
// Copyright 2018 The go-ethereum Authors | ||
// This file is part of the go-ethereum library. | ||
// | ||
// The go-ethereum library is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU Lesser General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// The go-ethereum library is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU Lesser General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU Lesser General Public License | ||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
package localstore | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum/log" | ||
"github.com/ethereum/go-ethereum/swarm/shed" | ||
"github.com/syndtr/goleveldb/leveldb" | ||
) | ||
|
||
var ( | ||
// gcTargetRatio defines the target number of items | ||
// in garbage collection index that will not be removed | ||
// on garbage collection. The target number of items | ||
// is calculated by gcTarget function. This value must be | ||
// in range (0,1]. For example, with 0.9 value, | ||
// garbage collection will leave 90% of defined capacity | ||
// in database after its run. This prevents frequent | ||
// garbage collection runt. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/runt/runs |
||
gcTargetRatio = 0.9 | ||
// gcBatchSize limits the number of chunks in a single | ||
// leveldb batch on garbage collection. | ||
gcBatchSize int64 = 1000 | ||
) | ||
|
||
// collectGarbageWorker is a long running function that waits for | ||
// collectGarbageTrigger channel to signal a garbage collection | ||
// run. GC run iterates on gcIndex and removes older items | ||
// form retrieval and other indexes. | ||
func (db *DB) collectGarbageWorker() { | ||
for { | ||
select { | ||
case <-db.collectGarbageTrigger: | ||
// run a single collect garbage run and | ||
// if done is false, gcBatchSize is reached and | ||
// another collect garbage run is needed | ||
collectedCount, done, err := db.collectGarbage() | ||
if err != nil { | ||
log.Error("localstore collect garbage", "err", err) | ||
} | ||
// check if another gc run is needed | ||
if !done { | ||
db.triggerGarbageCollection() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this worker needed and trigger needed? see my comment above There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
|
||
if testHookCollectGarbage != nil { | ||
testHookCollectGarbage(collectedCount) | ||
} | ||
case <-db.close: | ||
return | ||
} | ||
} | ||
} | ||
|
||
// collectGarbage removes chunks from retrieval and other | ||
// indexes if maximal number of chunks in database is reached. | ||
// This function returns the number of removed chunks. If done | ||
// is false, another call to this function is needed to collect | ||
// the rest of the garbage as the batch size limit is reached. | ||
// This function is called in collectGarbageWorker. | ||
func (db *DB) collectGarbage() (collectedCount int64, done bool, err error) { | ||
batch := new(leveldb.Batch) | ||
target := db.gcTarget() | ||
|
||
done = true | ||
err = db.gcIndex.Iterate(func(item shed.Item) (stop bool, err error) { | ||
zelig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// protect parallel updates | ||
unlock, err := db.lockAddr(item.Address) | ||
if err != nil { | ||
return false, err | ||
} | ||
defer unlock() | ||
|
||
gcSize := db.getGCSize() | ||
if gcSize-collectedCount <= target { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there not the danger of the following here:
"Loop" repeats What happens if we reach target while new chunks wait for write, is the write interrupted so that GC runs? What if DB is at 90%, then new chunks arrive with exceeding capacity, (say 105%), will write be interrupted? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume that you are referring only to a global lock. You can see that every chunk is saved in its own batch, so this can not happen, as gc should kick in. There should be one difference with global lock. The gc size counting should be much simpler and included in the chunk write batch. But this is only for the global lock, which is here only for BenchmarkPutUpload, until we decide whether we want to use it, or stick with address lock. |
||
return true, nil | ||
} | ||
// delete from retrieve, pull, gc | ||
db.retrievalDataIndex.DeleteInBatch(batch, item) | ||
db.retrievalAccessIndex.DeleteInBatch(batch, item) | ||
db.pullIndex.DeleteInBatch(batch, item) | ||
db.gcIndex.DeleteInBatch(batch, item) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can ANY of these There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. You can check how LevelDB handles batches. All errors are handled in its Write(batch) method. |
||
collectedCount++ | ||
if collectedCount >= gcBatchSize { | ||
// bach size limit reached, | ||
// another gc run is needed | ||
done = false | ||
return true, nil | ||
} | ||
return false, nil | ||
}, nil) | ||
if err != nil { | ||
return 0, false, err | ||
} | ||
|
||
err = db.shed.WriteBatch(batch) | ||
if err != nil { | ||
return 0, false, err | ||
} | ||
// batch is written, decrement gcSize | ||
db.incGCSize(-collectedCount) | ||
return collectedCount, done, nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you suggest where done parameter should be explained in more detail? There is a comment above in the code, but maybe it is badly written. The done parameter provides information if the garbage collection run reached the target. This can happen if the max gc batch size is reached. Do you have an idea how it can be implemented to be more clear? This should not cause problems, as gc size change is protected. If the global lock is released, we can allow more chunks to be saved, but they will not be collected as they will be on the top of the gc index, and gc iterates from the bottom of it. |
||
} | ||
|
||
// gcTrigger retruns the absolute value for garbage collection | ||
// target value, calculated from db.capacity and gcTargetRatio. | ||
func (db *DB) gcTarget() (target int64) { | ||
return int64(float64(db.capacity) * gcTargetRatio) | ||
} | ||
|
||
// incGCSize increments gcSize by the provided number. | ||
// If count is negative, it will decrement gcSize. | ||
func (db *DB) incGCSize(count int64) { | ||
if count == 0 { | ||
return | ||
} | ||
|
||
db.gcSizeMu.Lock() | ||
new := db.gcSize + count | ||
db.gcSize = new | ||
db.gcSizeMu.Unlock() | ||
|
||
select { | ||
case db.writeGCSizeTrigger <- struct{}{}: | ||
default: | ||
} | ||
if new >= db.capacity { | ||
db.triggerGarbageCollection() | ||
zelig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
// getGCSize returns gcSize value by locking it | ||
// with gcSizeMu mutex. | ||
func (db *DB) getGCSize() (count int64) { | ||
db.gcSizeMu.RLock() | ||
count = db.gcSize | ||
db.gcSizeMu.RUnlock() | ||
return count | ||
} | ||
|
||
// triggerGarbageCollection signals collectGarbageWorker | ||
// to call collectGarbage. | ||
func (db *DB) triggerGarbageCollection() { | ||
select { | ||
case db.collectGarbageTrigger <- struct{}{}: | ||
case <-db.close: | ||
default: | ||
} | ||
} | ||
|
||
// writeGCSizeWorker writes gcSize on trigger event | ||
// and waits writeGCSizeDelay after each write. | ||
// It implements a linear backoff with delay of | ||
// writeGCSizeDelay duration to avoid very frequent | ||
// database operations. | ||
func (db *DB) writeGCSizeWorker() { | ||
for { | ||
select { | ||
case <-db.writeGCSizeTrigger: | ||
err := db.writeGCSize(db.getGCSize()) | ||
if err != nil { | ||
log.Error("localstore write gc size", "err", err) | ||
} | ||
// Wait some time before writing gc size in the next | ||
// iteration. This prevents frequent I/O operations. | ||
select { | ||
frncmx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case <-time.After(10 * time.Second): | ||
case <-db.close: | ||
return | ||
} | ||
case <-db.close: | ||
return | ||
} | ||
} | ||
} | ||
|
||
// writeGCSize stores the number of items in gcIndex. | ||
// It removes all hashes from gcUncountedHashesIndex | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What Is More information please There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will try to explain it better in the comment in constructor. Do you think that there should be gcCountedHashesIndex? |
||
// not to include them on the next database initialization | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, maybe initialization is not the best word here. I will update the comment. It is meant when the node starts again (reboots). |
||
// when gcSize is counted. | ||
func (db *DB) writeGCSize(gcSize int64) (err error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe there should be some higher level description of how the process of GC looks like. What is I know you may have explained this during your walk through, but I already can't remember many details, also, I participated in the first walk through only, there we din't go into the details of GC. Also if a new member comes on board it would be useful There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will try to write documentation around gcSize persistence. I've added BenchmarkPutUpload to measure should we use global lock and reduce the complexity around gcSize counting. |
||
const maxBatchSize = 1000 | ||
|
||
batch := new(leveldb.Batch) | ||
db.storedGCSize.PutInBatch(batch, uint64(gcSize)) | ||
batchSize := 1 | ||
|
||
// use only one iterator as it acquires its snapshot | ||
// not to remove hashes from index that are added | ||
// after stored gc size is written | ||
err = db.gcUncountedHashesIndex.Iterate(func(item shed.Item) (stop bool, err error) { | ||
db.gcUncountedHashesIndex.DeleteInBatch(batch, item) | ||
batchSize++ | ||
if batchSize >= maxBatchSize { | ||
err = db.shed.WriteBatch(batch) | ||
if err != nil { | ||
return false, err | ||
frncmx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
batch.Reset() | ||
batchSize = 0 | ||
} | ||
return false, nil | ||
}, nil) | ||
if err != nil { | ||
return err | ||
} | ||
return db.shed.WriteBatch(batch) | ||
} | ||
|
||
// testHookCollectGarbage is a hook that can provide | ||
// information when a garbage collection run is done | ||
// and how many items it removed. | ||
var testHookCollectGarbage func(collectedCount int64) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/dwo/two