Skip to content

Commit 5143948

Browse files
authored
feat: add tiered blockstore (#238)
To enable use cases where blocks may be stored in more than one location, add a `TieredBlockstore` class to `blockstore-core` similar to the`TieredDatastore` class found in `datastore-core`.
1 parent 4279b47 commit 5143948

File tree

5 files changed

+246
-2
lines changed

5 files changed

+246
-2
lines changed

packages/blockstore-core/README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [BaseBlockstore](#baseblockstore)
1717
- [MemoryBlockstore](#memoryblockstore)
1818
- [BlackHoleBlockstore](#blackholeblockstore)
19+
- [TieredBlockstore](#tieredblockstore)
1920
- [API Docs](#api-docs)
2021
- [License](#license)
2122
- [Contribute](#contribute)
@@ -38,7 +39,8 @@ Loading this module through a script tag will make it's exports available as `Bl
3839

3940
- Base: [`src/base`](src/base.ts)
4041
- Memory: [`src/memory`](src/memory.ts)
41-
- BlackHole: ['src/blackhole](src/blackhole.ts)
42+
- BlackHole: ['src/black-hole](src/black-hole.ts)
43+
- Tiered: ['src/tiered](src/tiered.ts)
4244

4345
## Usage
4446

@@ -82,6 +84,22 @@ import { BlackHoleBlockstore } from 'blockstore-core/black-hole'
8284
const store = new BlackHoleBlockstore()
8385
```
8486

87+
### TieredBlockstore
88+
89+
A tiered blockstore wraps one or more blockstores and will query each in parallel to retrieve a block - the operation will succeed if any wrapped store has the block.
90+
91+
Writes are invoked on all wrapped blockstores.
92+
93+
```js
94+
import { TieredBlockstore } from 'blockstore-core/tiered'
95+
96+
const store = new TieredBlockstore([
97+
store1,
98+
store2,
99+
// ...etc
100+
])
101+
```
102+
85103
## API Docs
86104

87105
- <https://ipfs.github.io/js-stores/modules/blockstore_core.html>

packages/blockstore-core/package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
"./memory": {
6767
"types": "./dist/src/memory.d.ts",
6868
"import": "./dist/src/memory.js"
69+
},
70+
"./tiered": {
71+
"types": "./dist/src/tiered.d.ts",
72+
"import": "./dist/src/tiered.js"
6973
}
7074
},
7175
"eslintConfig": {
@@ -175,10 +179,16 @@
175179
"docs": "aegir docs"
176180
},
177181
"dependencies": {
182+
"@libp2p/logger": "^2.0.0",
178183
"err-code": "^3.0.1",
179184
"interface-blockstore": "^5.0.0",
180185
"interface-store": "^5.0.0",
181-
"multiformats": "^11.0.2"
186+
"it-drain": "^3.0.1",
187+
"it-filter": "^3.0.0",
188+
"it-merge": "^3.0.1",
189+
"it-pushable": "^3.0.0",
190+
"multiformats": "^11.0.2",
191+
"uint8arrays": "^4.0.2"
182192
},
183193
"devDependencies": {
184194
"aegir": "^39.0.9",

packages/blockstore-core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as ErrorsImport from './errors.js'
22

33
export { BaseBlockstore } from './base.js'
44
export { MemoryBlockstore } from './memory.js'
5+
export { BlackHoleBlockstore } from './black-hole.js'
6+
export { TieredBlockstore } from './tiered.js'
57

68
export const Errors = {
79
...ErrorsImport
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { logger } from '@libp2p/logger'
2+
import drain from 'it-drain'
3+
import filter from 'it-filter'
4+
import merge from 'it-merge'
5+
import { pushable } from 'it-pushable'
6+
import { BaseBlockstore } from './base.js'
7+
import * as Errors from './errors.js'
8+
import type { Blockstore, Pair } from 'interface-blockstore'
9+
import type { AbortOptions, AwaitIterable } from 'interface-store'
10+
import type { CID } from 'multiformats/cid'
11+
12+
const log = logger('blockstore:core:tiered')
13+
14+
/**
15+
* A blockstore that can combine multiple stores. Puts and deletes
16+
* will write through to all blockstores. Has and get will
17+
* try each store sequentially. getAll will use every store but also
18+
* deduplicate any yielded pairs.
19+
*/
20+
export class TieredBlockstore extends BaseBlockstore {
21+
private readonly stores: Blockstore[]
22+
23+
constructor (stores: Blockstore[]) {
24+
super()
25+
26+
this.stores = stores.slice()
27+
}
28+
29+
async put (key: CID, value: Uint8Array, options?: AbortOptions): Promise<CID> {
30+
try {
31+
await Promise.all(this.stores.map(async store => { await store.put(key, value, options) }))
32+
return key
33+
} catch (err: any) {
34+
throw Errors.putFailedError(err)
35+
}
36+
}
37+
38+
async get (key: CID, options?: AbortOptions): Promise<Uint8Array> {
39+
for (const store of this.stores) {
40+
try {
41+
const res = await store.get(key, options)
42+
if (res != null) return res
43+
} catch (err) {
44+
log.error(err)
45+
}
46+
}
47+
throw Errors.notFoundError()
48+
}
49+
50+
async has (key: CID, options?: AbortOptions): Promise<boolean> {
51+
for (const s of this.stores) {
52+
if (await s.has(key, options)) {
53+
return true
54+
}
55+
}
56+
57+
return false
58+
}
59+
60+
async delete (key: CID, options?: AbortOptions): Promise<void> {
61+
try {
62+
await Promise.all(this.stores.map(async store => { await store.delete(key, options) }))
63+
} catch (err: any) {
64+
throw Errors.deleteFailedError(err)
65+
}
66+
}
67+
68+
async * putMany (source: AwaitIterable<Pair>, options: AbortOptions = {}): AsyncIterable<CID> {
69+
let error: Error | undefined
70+
const pushables = this.stores.map(store => {
71+
const source = pushable<Pair>({
72+
objectMode: true
73+
})
74+
75+
drain(store.putMany(source, options))
76+
.catch(err => {
77+
// store threw while putting, make sure we bubble the error up
78+
error = err
79+
})
80+
81+
return source
82+
})
83+
84+
try {
85+
for await (const pair of source) {
86+
if (error != null) {
87+
throw error
88+
}
89+
90+
pushables.forEach(p => p.push(pair))
91+
92+
yield pair.cid
93+
}
94+
} finally {
95+
pushables.forEach(p => p.end())
96+
}
97+
}
98+
99+
async * deleteMany (source: AwaitIterable<CID>, options: AbortOptions = {}): AsyncIterable<CID> {
100+
let error: Error | undefined
101+
const pushables = this.stores.map(store => {
102+
const source = pushable<CID>({
103+
objectMode: true
104+
})
105+
106+
drain(store.deleteMany(source, options))
107+
.catch(err => {
108+
// store threw while deleting, make sure we bubble the error up
109+
error = err
110+
})
111+
112+
return source
113+
})
114+
115+
try {
116+
for await (const key of source) {
117+
if (error != null) {
118+
throw error
119+
}
120+
121+
pushables.forEach(p => p.push(key))
122+
123+
yield key
124+
}
125+
} finally {
126+
pushables.forEach(p => p.end())
127+
}
128+
}
129+
130+
async * getAll (options?: AbortOptions): AwaitIterable<Pair> { // eslint-disable-line require-yield
131+
// deduplicate yielded pairs
132+
const seen = new Set<string>()
133+
134+
yield * filter(merge(...this.stores.map(s => s.getAll(options))), (pair) => {
135+
const cidStr = pair.cid.toString()
136+
137+
if (seen.has(cidStr)) {
138+
return false
139+
}
140+
141+
seen.add(cidStr)
142+
143+
return true
144+
})
145+
}
146+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* eslint-env mocha */
2+
3+
import { expect } from 'aegir/chai'
4+
import { interfaceBlockstoreTests } from 'interface-blockstore-tests'
5+
import { CID } from 'multiformats/cid'
6+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
7+
import { MemoryBlockstore } from '../src/memory.js'
8+
import { TieredBlockstore } from '../src/tiered.js'
9+
import type { Blockstore } from 'interface-blockstore'
10+
11+
describe('Tiered', () => {
12+
describe('all stores', () => {
13+
const ms: Blockstore[] = []
14+
let store: TieredBlockstore
15+
beforeEach(() => {
16+
ms.push(new MemoryBlockstore())
17+
ms.push(new MemoryBlockstore())
18+
store = new TieredBlockstore(ms)
19+
})
20+
21+
it('put', async () => {
22+
const k = CID.parse('QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL')
23+
const v = uint8ArrayFromString('world')
24+
await store.put(k, v)
25+
const res = await Promise.all([ms[0].get(k), ms[1].get(k)])
26+
res.forEach((val) => {
27+
expect(val).to.be.eql(v)
28+
})
29+
})
30+
31+
it('get and has, where available', async () => {
32+
const k = CID.parse('QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL')
33+
const v = uint8ArrayFromString('world')
34+
await ms[1].put(k, v)
35+
const val = await store.get(k)
36+
expect(val).to.be.eql(v)
37+
const exists = await store.has(k)
38+
expect(exists).to.be.eql(true)
39+
})
40+
41+
it('has - key not found', async () => {
42+
expect(await store.has(CID.parse('QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnA'))).to.be.eql(false)
43+
})
44+
45+
it('has and delete', async () => {
46+
const k = CID.parse('QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL')
47+
const v = uint8ArrayFromString('world')
48+
await store.put(k, v)
49+
let res = await Promise.all([ms[0].has(k), ms[1].has(k)])
50+
expect(res).to.be.eql([true, true])
51+
await store.delete(k)
52+
res = await Promise.all([ms[0].has(k), ms[1].has(k)])
53+
expect(res).to.be.eql([false, false])
54+
})
55+
})
56+
57+
describe('inteface-datastore-single', () => {
58+
interfaceBlockstoreTests({
59+
setup () {
60+
return new TieredBlockstore([
61+
new MemoryBlockstore(),
62+
new MemoryBlockstore()
63+
])
64+
},
65+
teardown () { }
66+
})
67+
})
68+
})

0 commit comments

Comments
 (0)