Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit 730f67f

Browse files
committed
feat: GC
1 parent 7bf84d5 commit 730f67f

File tree

3 files changed

+177
-11
lines changed

3 files changed

+177
-11
lines changed

src/cli/commands/repo/gc.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,37 @@
11
'use strict'
22

3+
const { print } = require('../../utils')
4+
35
module.exports = {
46
command: 'gc',
57

68
describe: 'Perform a garbage collection sweep on the repo.',
79

8-
builder: {},
10+
builder: {
11+
quiet: {
12+
alias: 'q',
13+
desc: 'Write minimal output',
14+
type: 'boolean',
15+
default: false
16+
},
17+
'stream-errors': {
18+
desc: 'Output individual errors thrown when deleting blocks.',
19+
type: 'boolean',
20+
default: false
21+
}
22+
},
923

10-
handler (argv) {
11-
argv.resolve((async () => {
12-
const ipfs = await argv.getIpfs()
13-
await ipfs.repo.gc()
24+
handler ({ getIpfs, quiet, streamErrors, resolve }) {
25+
resolve((async () => {
26+
const ipfs = await getIpfs()
27+
const res = await ipfs.repo.gc()
28+
for (const r of res) {
29+
if (res.err) {
30+
streamErrors && print(res.err, true, true)
31+
} else {
32+
print((quiet ? '' : 'Removed ') + r.cid)
33+
}
34+
}
1435
})())
1536
}
1637
}

src/core/components/gc.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use strict'
2+
3+
const promisify = require('promisify-es6')
4+
const CID = require('cids')
5+
const base32 = require('base32.js')
6+
const parallel = require('async/parallel')
7+
const map = require('async/map')
8+
9+
module.exports = function gc (self) {
10+
return promisify(async (opts, callback) => {
11+
if (typeof opts === 'function') {
12+
callback = opts
13+
opts = {}
14+
}
15+
16+
const start = Date.now()
17+
self.log(`GC: Creating set of marked blocks`)
18+
19+
parallel([
20+
// Get all blocks from the blockstore
21+
(cb) => self._repo.blocks.query({ keysOnly: true }, cb),
22+
// Mark all blocks that are being used
23+
(cb) => createColoredSet(self, cb)
24+
], (err, [blocks, coloredSet]) => {
25+
if (err) {
26+
self.log(`GC: Error - ${err.message}`)
27+
return callback(err)
28+
}
29+
30+
// Delete blocks that are not being used
31+
deleteUnmarkedBlocks(self, coloredSet, blocks, start, (err, res) => {
32+
err && self.log(`GC: Error - ${err.message}`)
33+
callback(err, res)
34+
})
35+
})
36+
})
37+
}
38+
39+
// TODO: make global constants
40+
const { Key } = require('interface-datastore')
41+
const pinDataStoreKey = new Key('/local/pins')
42+
const MFS_ROOT_KEY = new Key('/local/filesroot')
43+
44+
function createColoredSet (ipfs, callback) {
45+
parallel([
46+
// "Empty block" used by the pinner
47+
(cb) => cb(null, ['QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n']),
48+
49+
// All pins, direct and indirect
50+
(cb) => ipfs.pin.ls((err, pins) => {
51+
if (err) {
52+
return cb(new Error(`Could not list pinned blocks: ${err.message}`))
53+
}
54+
ipfs.log(`GC: Found ${pins.length} pinned blocks`)
55+
cb(null, pins.map(p => p.hash))
56+
}),
57+
58+
// Blocks used internally by the pinner
59+
(cb) => ipfs._repo.datastore.get(pinDataStoreKey, (err, mh) => {
60+
if (err) {
61+
if (err.code === 'ERR_NOT_FOUND') {
62+
ipfs.log(`GC: No pinned blocks`)
63+
return cb(null, [])
64+
}
65+
return cb(new Error(`Could not get pin sets root from datastore: ${err.message}`))
66+
}
67+
68+
const cid = new CID(mh)
69+
ipfs.dag.get(cid, '', { preload: false }, (err, obj) => {
70+
// TODO: Handle not found?
71+
if (err) {
72+
return cb(new Error(`Could not get pin sets from store: ${err.message}`))
73+
}
74+
75+
// The pinner stores an object that has two links to pin sets:
76+
// 1. The directly pinned CIDs
77+
// 2. The recursively pinned CIDs
78+
cb(null, [cid.toString(), ...obj.value.links.map(l => l.cid.toString())])
79+
})
80+
}),
81+
82+
// The MFS root and all its descendants
83+
(cb) => ipfs._repo.datastore.get(MFS_ROOT_KEY, (err, mh) => {
84+
if (err) {
85+
if (err.code === 'ERR_NOT_FOUND') {
86+
ipfs.log(`GC: No blocks in MFS`)
87+
return cb(null, [])
88+
}
89+
return cb(new Error(`Could not get MFS root from datastore: ${err.message}`))
90+
}
91+
92+
getDescendants(ipfs, new CID(mh), cb)
93+
})
94+
], (err, res) => callback(err, !err && new Set(res.flat())))
95+
}
96+
97+
function getDescendants (ipfs, cid, callback) {
98+
// TODO: Make sure we don't go out to the network
99+
ipfs.refs(cid, { recursive: true }, (err, refs) => {
100+
if (err) {
101+
return callback(new Error(`Could not get MFS root descendants from store: ${err.message}`))
102+
}
103+
ipfs.log(`GC: Found ${refs.length} MFS blocks`)
104+
callback(null, [cid.toString(), ...refs.map(r => r.ref)])
105+
})
106+
}
107+
108+
function deleteUnmarkedBlocks (ipfs, coloredSet, blocks, start, callback) {
109+
// Iterate through all blocks and find those that are not in the marked set
110+
// The blocks variable has the form { { key: Key() }, { key: Key() }, ... }
111+
const unreferenced = []
112+
const res = []
113+
for (const { key: k } of blocks) {
114+
try {
115+
const cid = dsKeyToCid(k)
116+
if (!coloredSet.has(cid.toString())) {
117+
unreferenced.push(cid)
118+
}
119+
} catch (err) {
120+
res.push({ err: new Error(`Could not convert block with key '${k}' to CID: ${err.message}`) })
121+
}
122+
}
123+
124+
const msg = `GC: Marked set has ${coloredSet.size} blocks. Blockstore has ${blocks.length} blocks. ` +
125+
`Deleting ${unreferenced.length} blocks.`
126+
ipfs.log(msg)
127+
128+
// TODO: limit concurrency
129+
map(unreferenced, (cid, cb) => {
130+
// Delete blocks from blockstore
131+
ipfs._repo.blocks.delete(cid, (err) => {
132+
const res = {
133+
cid: cid.toString(),
134+
err: err && new Error(`Could not delete block with CID ${cid}: ${err.message}`)
135+
}
136+
cb(null, res)
137+
})
138+
}, (_, delRes) => {
139+
ipfs.log(`GC: Complete (${Date.now() - start}ms)`)
140+
141+
callback(null, res.concat(delRes))
142+
})
143+
}
144+
145+
function dsKeyToCid (key) {
146+
// Block key is of the form /<base32 encoded string>
147+
const decoder = new base32.Decoder()
148+
const buff = decoder.write(key.toString().slice(1)).finalize()
149+
return new CID(buff)
150+
}

src/core/components/repo.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,7 @@ module.exports = function repo (self) {
3939
}),
4040

4141
gc: promisify((options, callback) => {
42-
if (typeof options === 'function') {
43-
callback = options
44-
options = {}
45-
}
46-
47-
callback(new Error('Not implemented'))
42+
require('./gc')(self)(options, callback)
4843
}),
4944

5045
stat: promisify((options, callback) => {

0 commit comments

Comments
 (0)