Skip to content

Commit 5933064

Browse files
committed
Store txn in AsyncLocalStorage context
1 parent 8673df9 commit 5933064

1 file changed

Lines changed: 105 additions & 74 deletions

File tree

store.js

Lines changed: 105 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
const { pack, unpack } = require('msgpackr')
2-
const { mkdirSync } = require('fs')
3-
const { Cursor, Env } = require('node-gyp-build')(__dirname)
1+
const { pack, unpack } = require('msgpackr')
2+
const { mkdirSync } = require('fs')
3+
const { Cursor, Env } = require('node-gyp-build')(__dirname)
4+
const { AsyncLocalStorage } = require('async_hooks')
45

56
function asBinary(buffer) {
67
return {
@@ -69,138 +70,168 @@ class Iterator {
6970
}
7071

7172
/**
72-
* This class enforces a very specific usage of lmdb. All keys are utf8 encoded
73-
* buffers and all values are encoded with msgpack. Transaction handling is also
74-
* intentionally simplified to just one current transaction, no nesting, with
75-
* the assumption that there is only one active client. This is the pattern that
73+
* This class enforces a very specific usage of lmdb. All keys are
74+
* utf8 encoded buffers and all values are encoded with
75+
* msgpack. Transaction handling is also intentionally simplified to
76+
* just one current transaction, no nesting. This is the pattern that
7677
* adset-consumer uses in its current form.
7778
*/
7879
class Store {
7980
// TODO: add in support for a persistent readonly transaction for the lifetime
8081
// of the Store instance - Engines will need this
8182
// TODO: maybe refactor to split env/dbi params
8283
// TODO: support additional dbis
84+
85+
/**
86+
* @param {object} options
87+
* @param {boolean} options.create
88+
* @param {number} options.mapSize
89+
* @param {string} options.name
90+
* @param {boolean} options.noReadAhead
91+
* @param {string} options.path
92+
* @param {AsyncLocalStorage} context
93+
*/
8394
constructor({
8495
create = false,
8596
mapSize,
8697
name,
8798
noReadAhead = false,
8899
path
89-
}) {
100+
}, context) {
90101
this.env = new Env()
91102
this.env.open({ path, mapSize, noReadAhead })
92103
this.dbi = this.env.openDbi({ name, create, keyIsBuffer: true })
93-
this.txn = null
104+
105+
if (context) {
106+
this.context = context
107+
} else {
108+
console.log('no context supplied, creating our own local storage context')
109+
this.context = new AsyncLocalStorage()
110+
}
94111
}
95112

113+
/**
114+
*
115+
* @param {string} key
116+
* @param {object} value
117+
*/
96118
put(key, value) {
97-
this.transact(() => {
98-
try {
99-
const keyBuffer = Buffer.from(key, 'utf8')
100-
let valueBuffer
101-
if (value && value['\x10binary-data\x02'])
102-
valueBuffer = value['\x10binary-data\x02']
103-
else
104-
valueBuffer = pack(value)
105-
this.txn.putBinary(this.dbi, keyBuffer, valueBuffer)
106-
} catch (error) {
107-
console.error('Error storing value:', error)
108-
}
119+
this.transact((txn) => {
120+
const keyBuffer = Buffer.from(key, 'utf8')
121+
let valueBuffer
122+
if (value && value['\x10binary-data\x02'])
123+
valueBuffer = value['\x10binary-data\x02']
124+
else
125+
valueBuffer = pack(value)
126+
txn.putBinary(this.dbi, keyBuffer, valueBuffer)
109127
})
110128
}
111129

130+
/**
131+
*
132+
* @param {object} key
133+
* @returns
134+
*/
112135
get(key) {
113-
return this.transact(() => {
114-
try {
115-
const keyBuffer = Buffer.from(key, 'utf8')
116-
const value = this.txn.getBinary(this.dbi, keyBuffer)
117-
return value == null ? null : unpack(value)
118-
} catch (error) {
119-
console.error('Error retrieving value:', error)
120-
return null
121-
}
136+
return this.transact((txn) => {
137+
const keyBuffer = Buffer.from(key, 'utf8')
138+
const value = txn.getBinary(this.dbi, keyBuffer)
139+
return value == null ? null : unpack(value)
122140
}, true)
123141
}
124142

143+
/**
144+
*
145+
* @param {object} key
146+
* @returns
147+
*/
125148
del(key) {
126-
return this.transact(() => {
127-
try {
128-
const keyBuffer = Buffer.from(key, 'utf8')
129-
if (this.txn.getBinary(this.dbi, keyBuffer) != null) {
130-
this.txn.del(this.dbi, keyBuffer)
131-
return true
132-
} else {
133-
return false
134-
}
135-
} catch (error) {
136-
console.error('Error deleting value:', error)
149+
return this.transact((txn) => {
150+
const keyBuffer = Buffer.from(key, 'utf8')
151+
if (txn.getBinary(this.dbi, keyBuffer) != null) {
152+
txn.del(this.dbi, keyBuffer)
153+
return true
154+
} else {
155+
return false
137156
}
138157
})
139158
}
140159

141160
/**
142-
* Wrapper function for a transaction. This is only sensible in adset-consumer
143-
* as there is only ever one thread of control working on the Store instance
144-
* at a time.
145-
* @param {*} f
161+
* Wrapper function for a transaction.
162+
* @param {*} f Function to execute within a txn
163+
* @param {boolean} [readOnly=false] Set to true if txn is readOnly
146164
*/
147-
transact(f, readonly = false) {
165+
transact(f, readOnly = false) {
148166
let ownTxn = false
149-
if (!this.txn) {
150-
this.txn = this.env.beginTxn()
167+
let store = this.context.getStore()
168+
if (!store) {
169+
store = {}
170+
this.context.enterWith(store)
171+
}
172+
let txn = store.txn
173+
if (!txn) {
174+
txn = this.env.beginTxn({ readOnly })
175+
store.txn = txn
151176
ownTxn = true
152177
}
153178

154179
try {
155-
const result = f()
180+
const result = f(txn)
156181
if (ownTxn) {
157-
if (readonly) {
158-
this.txn.abort()
159-
}
160-
else {
161-
this.txn.commit()
162-
}
182+
if (readOnly)
183+
txn.abort()
184+
else
185+
txn.commit()
163186
}
164187
return result
165188
} catch (error) {
166-
console.error('Transaction aborted due to an error:', error.message)
167-
this.txn.abort()
189+
if (ownTxn) {
190+
console.error('transaction aborted:', error.message)
191+
txn.abort()
192+
}
168193
throw error
169194
} finally {
170195
if (ownTxn)
171-
this.txn = null
196+
store.txn = null
172197
}
173198
}
174199

175-
async transactAsync(f, readonly = false) {
200+
async transactAsync(f, readOnly = false) {
176201
let ownTxn = false
177-
if (!this.txn) {
178-
this.txn = this.env.beginTxn()
202+
let store = this.context.getStore()
203+
if (!store) {
204+
store = {}
205+
this.context.enterWith(store)
206+
}
207+
let txn = store.txn
208+
if (!txn) {
209+
txn = this.env.beginTxn({ readOnly })
210+
store.txn = txn
179211
ownTxn = true
180212
}
181213

182214
try {
183-
const result = await f()
215+
const result = await f(txn)
184216
if (ownTxn) {
185-
if (readonly) {
186-
this.txn.abort()
187-
}
188-
else {
189-
this.txn.commit()
190-
}
217+
if (readOnly)
218+
txn.abort()
219+
else
220+
txn.commit()
191221
}
192222
return result
193223
} catch (error) {
194-
console.error('Transaction aborted due to an error:', error.message)
195-
this.txn.abort()
224+
if (ownTxn) {
225+
console.error('transaction aborted:', error.message)
226+
txn.abort()
227+
}
196228
throw error
197229
} finally {
198230
if (ownTxn)
199-
this.txn = null
231+
store.txn = null
200232
}
201233
}
202234

203-
204235
iterate() {
205236
return new Iterator(this.env, this.dbi)
206237
}
@@ -239,8 +270,8 @@ class Store {
239270
}
240271

241272
getCount() {
242-
return this.transact(() => {
243-
return this.dbi.stat(this.txn)?.entryCount
273+
return this.transact((txn) => {
274+
return this.dbi.stat(txn)?.entryCount
244275
})
245276
}
246277

0 commit comments

Comments
 (0)