Skip to content

Commit 8b3f268

Browse files
more tests! more everything.
1 parent 3156600 commit 8b3f268

11 files changed

+520
-117
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
coverage/

db-session.js

Lines changed: 76 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const DOMAIN_TO_SESSION = new WeakMap()
44
const Promise = require('bluebird')
55
const domain = require('domain')
66

7-
const AtomicSessionConnectionPair = require('./lib/atomic-session-connpair.js')
87
const TxSessionConnectionPair = require('./lib/tx-session-connpair.js')
98
const SessionConnectionPair = require('./lib/session-connpair.js')
109

@@ -43,17 +42,14 @@ const api = module.exports = {
4342
},
4443

4544
getConnection () {
46-
return DOMAIN_TO_SESSION.get(process.domain).getConnection()
45+
return api.session.getConnection()
4746
},
4847

4948
get session () {
5049
var current = DOMAIN_TO_SESSION.get(process.domain)
51-
if (!current || !process.domain) {
50+
if (!current || current.inactive || !process.domain) {
5251
throw new NoSessionAvailable()
5352
}
54-
while (current.inactive && current.parent) {
55-
current = current.parent
56-
}
5753
return current
5854
},
5955

@@ -93,15 +89,20 @@ class Session {
9389

9490
transaction (operation, args) {
9591
const getConnPair = this.getConnection()
96-
const getResult = Session$RunWrapped(this, connPair => {
97-
return new TransactionSession(this, connPair)
92+
const getResult = Session$RunWrapped(connPair => {
93+
return new TransactionSession(connPair)
9894
}, getConnPair, `BEGIN`, {
9995
success: `COMMIT`,
10096
failure: `ROLLBACK`
10197
}, operation, args)
102-
const releasePair = getResult.return(getConnPair).then(
103-
pair => pair.release()
104-
)
98+
99+
const releasePair = getConnPair.then(pair => {
100+
return getResult.reflect().then(result => {
101+
return result.isFulfilled()
102+
? pair.release()
103+
: pair.release(result.reason())
104+
})
105+
})
105106

106107
return releasePair.return(getResult)
107108
}
@@ -119,97 +120,107 @@ class Session {
119120
}
120121

121122
class TransactionSession {
122-
constructor (parent, connPair) {
123-
this.parent = parent
123+
constructor (connPair) {
124124
this.connectionPair = connPair
125125
this.inactive = false
126126
this.operation = Promise.resolve(true)
127127
}
128128

129129
getConnection () {
130130
if (this.inactive) {
131-
return this.parent.getConnection()
131+
return new Promise((_, reject) => {
132+
reject(new NoSessionAvailable())
133+
})
132134
}
133-
// XXX(chrisdickinson): creating a TxConnPair implicitly
135+
// NB(chrisdickinson): creating a TxConnPair implicitly
134136
// swaps out "this.operation", creating a linked list of
135137
// promises.
136138
return new TxSessionConnectionPair(this).onready
137139
}
138140

139141
transaction (operation, args) {
140142
if (this.inactive) {
141-
return this.parent.transaction(operation, args)
143+
return new Promise((_, reject) => {
144+
reject(new NoSessionAvailable())
145+
})
142146
}
143147
return operation.apply(null, args)
144148
}
145149

146150
atomic (operation, args) {
147-
const atomicConnPair = new AtomicSessionConnectionPair(this)
151+
const atomicConnPair = this.getConnection()
148152
const savepointName = getSavepointName(operation)
149-
const getResult = Session$RunWrapped(this, connPair => {
150-
return new AtomicSession(this, connPair, savepointName)
151-
}, atomicConnPair.onready, `SAVEPOINT ${savepointName}`, {
153+
const getResult = Session$RunWrapped(connPair => {
154+
return new AtomicSession(connPair, savepointName)
155+
}, atomicConnPair, `SAVEPOINT ${savepointName}`, {
152156
success: `RELEASE SAVEPOINT ${savepointName}`,
153157
failure: `ROLLBACK TO SAVEPOINT ${savepointName}`
154158
}, operation, args)
155159

156-
return getResult.then(() => {
157-
setImmediate(() => {
158-
atomicConnPair.close()
160+
const releasePair = atomicConnPair.then(pair => {
161+
return getResult.reflect().then(result => {
162+
return result.isFulfilled()
163+
? pair.release()
164+
: pair.release(result.reason())
159165
})
160-
}).return(getResult)
166+
})
167+
168+
return releasePair.return(getResult)
161169
}
162170
}
163171

164172
class AtomicSession extends TransactionSession {
165-
constructor (parent, connection, name) {
166-
super(parent, connection)
173+
constructor (connection, name) {
174+
super(connection)
167175
this.name = name
168176
}
169177
}
170178

171-
function Session$RunWrapped (parent,
172-
createSession,
173-
getConnPair, before, after, operation, args) {
174-
const createSubdomain = getConnPair.then(connPair => {
179+
function Session$RunWrapped (createSession,
180+
getConnPair,
181+
before,
182+
after,
183+
operation,
184+
args) {
185+
return getConnPair.then(pair => {
175186
const subdomain = domain.create()
176-
const session = createSession(connPair)
187+
const session = createSession(pair)
177188
DOMAIN_TO_SESSION.set(subdomain, session)
178-
return subdomain
179-
})
180189

181-
const runBefore = getConnPair.then(connPair => new Promise(
182-
(resolve, reject) => connPair.connection.query(
183-
before,
184-
err => err ? reject(err) : resolve()
185-
)
186-
))
190+
const runBefore = new Promise((resolve, reject) => {
191+
return pair.connection.query(
192+
before,
193+
err => err ? reject(err) : resolve()
194+
)
195+
})
187196

188-
const getResult = runBefore.return(
189-
createSubdomain
190-
).then(domain => {
191-
args.unshift(operation)
192-
return Promise.resolve(domain.run.apply(domain, args))
193-
})
197+
return runBefore.then(() => {
198+
const opArgs = args.slice()
199+
opArgs.unshift(operation)
200+
const getResult = Promise.resolve(
201+
subdomain.run.apply(subdomain, opArgs)
202+
)
194203

195-
const getReflectedResult = getResult.reflect()
196-
const runCommitStep = Promise.join(
197-
getReflectedResult,
198-
getConnPair.get('connection')
199-
).spread((result, connection) => {
200-
return new Promise((resolve, reject) => {
201-
connection.query(
202-
result.isFulfilled()
203-
? after.success
204-
: after.failure,
205-
err => err ? reject(err) : resolve()
204+
const waitOperation = Promise.join(
205+
getResult,
206+
getResult.then(() => session.operation)
206207
)
208+
.finally(markInactive(subdomain))
209+
.return(getResult.reflect())
210+
211+
const runCommitStep = waitOperation.then(result => {
212+
return new Promise((resolve, reject) => {
213+
return pair.connection.query(
214+
result.isFulfilled()
215+
? after.success
216+
: after.failure,
217+
err => err ? reject(err) : resolve()
218+
)
219+
})
220+
})
221+
return runCommitStep.return(getResult)
207222
})
208223
})
209-
210-
return runCommitStep.return(
211-
createSubdomain
212-
).then(markInactive(parent)).return(getResult)
213224
}
214225

215226
function getSavepointName (operation) {
@@ -221,14 +232,11 @@ function getSavepointName (operation) {
221232
}
222233
getSavepointName.ID = 0
223234

224-
function markInactive (session) {
225-
return domain => {
226-
domain.exit()
227-
DOMAIN_TO_SESSION.get(domain).inactive = true
228-
229-
// if, somehow, we get a reference to this domain again, point
230-
// it at the parent session.
231-
DOMAIN_TO_SESSION.set(domain, session)
235+
function markInactive (subdomain) {
236+
return () => {
237+
subdomain.exit()
238+
DOMAIN_TO_SESSION.get(subdomain).inactive = true
239+
DOMAIN_TO_SESSION.set(subdomain, null)
232240
}
233241
}
234242

lib/atomic-session-connpair.js

Lines changed: 0 additions & 33 deletions
This file was deleted.

lib/session-connpair.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,11 @@ function release (conn, err) {
3434
// with the error notification; replay all pending connections
3535
// so they don't try to grab this one.
3636
function handleError (conn, err) {
37-
conn.session.decrementConnections()
3837
conn.session.releasePair(conn.pair, err)
3938

4039
const pending = conn.session.pending.slice()
41-
conn.session.length = 0
42-
while (pending.length) {
43-
pending.shift().resolve(conn.session.getConnection())
44-
}
40+
conn.session.pending.length = 0
41+
pending.forEach(xs => {
42+
conn.session.getConnection().then(ys => xs.resolve(ys))
43+
})
4544
}

lib/tx-session-connpair.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,39 @@ const Promise = require('bluebird')
55
const RESOLVE_SYM = Symbol()
66
const REJECT_SYM = Symbol()
77

8+
// while SessionConnectionPair (./session-connpair.js) represents
9+
// an *actual* postgres connection and "release" function,
10+
// TransactionSessionConnPair *wraps* a SessionConnectionPair in
11+
// order to serialize access to it. As such, "txPair.release()"
12+
// doesn't release the underlying connection, it just lets
13+
// subsequent operations run.
14+
//
15+
// the session machinery handles "fully" releasing the underlying
16+
// connection — see Session#transaction and TransactionSession#atomic
17+
// for more details (specifically, look for "release()".)
818
module.exports = class TransactionSessionConnPair {
919
constructor (session) {
1020
this.pair = session.connectionPair
1121
this.release = err => release(this, err)
1222
this.completed = new Promise((resolve, reject) => {
1323
this[RESOLVE_SYM] = resolve
1424
this[REJECT_SYM] = reject
25+
}).catch((/* err */) => {
26+
// XXX(chrisdickinson): this would be the place to
27+
// add error monitoring.
1528
})
29+
30+
// the onready promise will let session methods know
31+
// that previous operations have fully resolved —
32+
// "session.operation" is "txPair.completed", so we end
33+
// up with a linked list of promises, e.g.:
34+
//
35+
// duration of one transaction
36+
// _ _ _ _ _ _ _|_ _ _ _ _ _ _
37+
// / \
38+
// | |
39+
// completed → onready → (work happens) → completed → onready
40+
//
1641
this.onready = session.operation.then(() => this)
1742
session.operation = this.completed
1843
}

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
"description": "domain-attached database sessions",
55
"main": "db-session.js",
66
"scripts": {
7-
"test": "tap test/*-test.js && standard",
8-
"test-cov": "nyc tap test/*-test.js && standard"
7+
"test:code": "tap test/*-test.js",
8+
"test:style": "standard",
9+
"test": "npm run test:code && npm run test:style",
10+
"cov:test": "nyc npm run test:code",
11+
"cov:html": "npm run cov:test && nyc report --reporter=lcov",
12+
"cov:view": "npm run cov:html && ecstatic coverage/lcov-report -p 60888"
913
},
1014
"repository": {
1115
"type": "git",
@@ -24,6 +28,7 @@
2428
},
2529
"homepage": "https://github.com/npm/pg-db-session#readme",
2630
"devDependencies": {
31+
"ecstatic": "^1.4.0",
2732
"nyc": "^5.3.0",
2833
"pg": "^4.4.3",
2934
"standard": "^5.4.1",

0 commit comments

Comments
 (0)