Skip to content

Commit 07c8b6e

Browse files
Detect when a connection is closed before it's done opening
- close connections explicitly in test code
1 parent acd4c04 commit 07c8b6e

File tree

6 files changed

+72
-31
lines changed

6 files changed

+72
-31
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sqlitecloud-js",
3-
"version": "0.0.22-beta",
3+
"version": "0.0.23",
44
"description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients",
55
"main": "./lib/index.js",
66
"types": "./lib/index.d.ts",

src/connection.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
* connection.ts - handles low level communication with sqlitecloud server
33
*/
44

5-
import tls from 'tls'
5+
import tls, { TLSSocket } from 'tls'
66

77
const lz4 = require('lz4js')
88

99
import { SQLiteCloudConfig, SQLCloudRowsetMetadata, SQLiteCloudError, SQLiteCloudDataTypes, ErrorCallback, ResultsCallback } from './types'
1010
import { SQLiteCloudRowset } from './rowset'
1111
import { parseConnectionString, parseBoolean } from './utilities'
12+
import { Socket } from 'net'
1213

1314
/**
1415
* The server communicates with clients via commands defined
@@ -59,7 +60,7 @@ export class SQLiteCloudConnection {
5960
private config: SQLiteCloudConfig
6061

6162
/** Currently opened tls socket used to communicated with SQLiteCloud server */
62-
private socket?: tls.TLSSocket
63+
private socket?: tls.TLSSocket | null
6364

6465
/** Operations are serialized by waiting an any pending promises */
6566
private operations = new OperationsQueue()
@@ -70,7 +71,7 @@ export class SQLiteCloudConnection {
7071

7172
/** True if connection is open */
7273
public get connected(): boolean {
73-
return this.socket !== undefined
74+
return !!this.socket
7475
}
7576

7677
//
@@ -154,7 +155,7 @@ export class SQLiteCloudConnection {
154155
private connect(callback?: ErrorCallback): this {
155156
this.operations.enqueue(done => {
156157
// connection established while we were waiting in line?
157-
console.assert(this.socket === undefined, 'Connection already established')
158+
console.assert(!this.connected, 'Connection already established')
158159

159160
// clear all listeners and call done in the operations queue
160161
const finish: ResultsCallback = error => {
@@ -170,16 +171,23 @@ export class SQLiteCloudConnection {
170171
}
171172

172173
// connect to tls socket, initialize connection, setup event handlers
173-
const client: tls.TLSSocket = tls.connect(this.config.port as number, this.config.host, this.config.tlsOptions, () => {
174-
if (!client.authorized) {
175-
const anonimizedError = anonimizeError(client.authorizationError)
174+
this.socket = tls.connect(this.config.port as number, this.config.host, this.config.tlsOptions, () => {
175+
if (!this.socket?.authorized) {
176+
const anonimizedError = anonimizeError((this.socket as TLSSocket).authorizationError)
176177
this.log('Connection was not authorized', anonimizedError)
177178
this.close()
178179
finish(new SQLiteCloudError('Connection was not authorized', { cause: anonimizedError }))
179180
} else {
180-
this.socket = client
181+
// the connection was closed before it was even opened,
182+
// eg. client closed the connection before the server accepted it
183+
if (this.socket === null) {
184+
this.log('Connection was closed before it was opened')
185+
finish(new SQLiteCloudError('Connection was closed before it was done opening'))
186+
return
187+
}
181188

182189
// send initialization commands
190+
console.assert(this.socket, 'Connection already closed')
183191
const commands = this.initializationCommands
184192
this.processCommands(commands, error => {
185193
if (error) {
@@ -194,17 +202,18 @@ export class SQLiteCloudConnection {
194202
}
195203
})
196204

197-
client.on('close', () => {
205+
this.socket.on('close', () => {
198206
this.log('Connection closed')
199-
this.socket = undefined
207+
this.socket = null
200208
finish(new SQLiteCloudError('Connection was closed'))
201209
})
202210

203-
client.once('error', (error: any) => {
211+
this.socket.once('error', (error: any) => {
204212
this.log('Connection error', error)
205213
finish(new SQLiteCloudError('Connection error', { cause: error }))
206214
})
207215
})
216+
208217
return this
209218
}
210219

@@ -348,8 +357,10 @@ export class SQLiteCloudConnection {
348357

349358
/** Disconnect from server, release connection. */
350359
public close(): this {
360+
console.assert(this.socket !== undefined, 'SQLiteCloudConnection.close - connection already closed')
351361
if (this.socket) {
352362
this.socket.destroy()
363+
this.socket = null
353364
}
354365
this.operations.clear()
355366
this.socket = undefined

test/compare.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getChinookDatabase, getTestingDatabase } from './shared'
88
// https://github.com/TryGhost/node-sqlite3/wiki/API
99
import sqlite3 from 'sqlite3'
1010
import { join } from 'path'
11+
import { error } from 'console'
1112

1213
const INSERT_SQL = "INSERT INTO people (name, hobby, age) VALUES ('Fantozzi Ugo', 'Competitive unicorn farting', 42); "
1314
const TESTING_DATABASE_FILE = join(__dirname, 'assets/testing.db')
@@ -81,14 +82,30 @@ describe('Database.on', () => {
8182
chinookFile.close()
8283
})
8384

84-
it('sqlitecloud: should emit close event', done => {
85+
it('sqlitecloud: should close before it finishes opening', done => {
8586
const chinookCloud = getChinookDatabase()
8687
chinookCloud.once('close', () => {
8788
done()
8889
})
90+
91+
// we are closing the connection before it's really had a chance to open...
8992
chinookCloud.close()
9093
})
9194

95+
it('sqlitecloud: should emit close event', done => {
96+
const chinookCloud = getChinookDatabase((error, database) => {
97+
// we first wait for the database to open, then close it
98+
expect(error).toBeNull()
99+
100+
chinookCloud.once('close', () => {
101+
done()
102+
})
103+
104+
// we are closing the connection asynchonously
105+
chinookCloud.close()
106+
})
107+
})
108+
92109
// end Database.on
93110
})
94111

test/connection.test.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,20 @@
44

55
import { SQLiteCloudError } from '../src/index'
66
import { SQLiteCloudConnection, anonimizeCommand } from '../src/connection'
7-
import { parseConnectionString } from '../src/utilities'
87
import {
98
CHINOOK_DATABASE_URL,
109
LONG_TIMEOUT,
1110
getTestingConfig,
1211
getChinookConfig,
1312
getChinookConnection,
13+
// clearTestingDatabasesAsync,
1414
WARN_SPEED_MS,
1515
EXPECT_SPEED_MS
16-
// clearTestingDatabasesAsync
1716
} from './shared'
1817

1918
describe('connection', () => {
2019
let chinook: SQLiteCloudConnection
21-
/*
22-
beforeAll(async () => {
23-
24-
})
25-
*/
20+
2621
beforeEach(() => {
2722
chinook = getChinookConnection()
2823
})
@@ -37,13 +32,15 @@ describe('connection', () => {
3732
it('should connect', () => {
3833
// ...in beforeEach
3934
})
40-
4135
/*
42-
it('should run manually once in a while to clear orphaned databases', async () => {
43-
await clearTestingDatabasesAsync()
44-
})
45-
*/
46-
36+
it(
37+
'should drop all old testing databases',
38+
async () => {
39+
await clearTestingDatabasesAsync()
40+
},
41+
LONG_TIMEOUT
42+
)
43+
*/
4744
it('should add self signed certificate for localhost connections', () => {
4845
const localConfig = getChinookConfig('sqlitecloud://admin:xxx@localhost:8850/chinook.db')
4946
expect(localConfig.host).toBe('localhost')

test/stress.test.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ describe('stress testing', () => {
7777
const connections: Database[] = []
7878

7979
try {
80+
// open connection
8081
for (let i = 0; i < SIMULTANEOUS_TEST_SIZE; i++) {
8182
connections.push(
8283
getChinookDatabase(error => {
@@ -87,12 +88,16 @@ describe('stress testing', () => {
8788
})
8889
)
8990
}
91+
92+
// use connection
9093
for (let i = 0; i < SIMULTANEOUS_TEST_SIZE; i++) {
9194
const connection = connections[i]
9295
expect(connection).not.toBeNull()
9396
const results = await connection.sql`SELECT ${i} as 'connection_id'`
9497
expect(results[0]['connection_id']).toBe(i)
9598
}
99+
100+
// close connection
96101
for (let i = 0; i < SIMULTANEOUS_TEST_SIZE; i++) {
97102
connections[i].close()
98103
}
@@ -115,10 +120,11 @@ describe('stress testing', () => {
115120
async () => {
116121
const numQueries = 20
117122
const startTime = Date.now()
118-
const database = getChinookDatabase()
123+
const chinook = getChinookDatabase()
124+
119125
const table = 'tracks'
120126
for (let i = 0; i < SEQUENCE_TEST_SIZE; i++) {
121-
const results = await database.sql`SELECT * FROM ${table} ORDER BY RANDOM() LIMIT 12`
127+
const results = await chinook.sql`SELECT * FROM ${table} ORDER BY RANDOM() LIMIT 12`
122128
expect(results).toHaveLength(12)
123129
expect(Object.keys(results[0])).toEqual(['TrackId', 'Name', 'AlbumId', 'MediaTypeId', 'GenreId', 'Composer', 'Milliseconds', 'Bytes', 'UnitPrice'])
124130
}
@@ -128,6 +134,8 @@ describe('stress testing', () => {
128134
console.warn(`${numQueries}x database.sql selects, ${queryMs.toFixed(0)}ms per query`)
129135
expect(queryMs).toBeLessThan(EXPECT_SPEED_MS)
130136
}
137+
138+
chinook.close()
131139
},
132140
EXTRA_LONG_TIMEOUT
133141
)
@@ -140,6 +148,8 @@ describe('stress testing', () => {
140148
expect(smallResults).toHaveLength(1)
141149
expect(smallResults.metadata.columns[0].name).toBe('small')
142150
expect(smallResults[0].small).toHaveLength(2 * 1000) // hex encoded
151+
152+
chinook.close()
143153
})
144154

145155
it(
@@ -153,6 +163,8 @@ describe('stress testing', () => {
153163
expect(largeResults.metadata.columns[0].name).toBe('columName')
154164
const largeResultString = largeResults[0].columName as string
155165
expect(largeResultString).toHaveLength(2 * 10000000) // 20 MB
166+
167+
chinook.close()
156168
},
157169
EXTRA_LONG_TIMEOUT
158170
)
@@ -171,14 +183,16 @@ describe('stress testing', () => {
171183
expect(largeCompressedResults).toHaveLength(1)
172184
expect(largeCompressedResults.metadata.columns[0].name).toBe('columnName')
173185
expect(largeCompressedResults[0].columnName).toHaveLength(2 * blobSize)
186+
187+
chinook.close()
174188
},
175189
EXTRA_LONG_TIMEOUT
176190
)
177191

178192
it(
179193
'should receive large responses with compressed data',
180194
async () => {
181-
const chinook = getChinookDatabase()
195+
const chinook = getChinookDatabase(undefined, { timeout: EXTRA_LONG_TIMEOUT })
182196

183197
// enable compression
184198
const enable = await chinook.sql`SET CLIENT KEY COMPRESSION TO 1;`
@@ -193,6 +207,8 @@ describe('stress testing', () => {
193207
const postfix = await chinook.sql`SELECT 1`
194208
expect(postfix).toHaveLength(1)
195209
expect(postfix[0]['1']).toBe(1)
210+
211+
chinook.close()
196212
},
197213
EXTRA_LONG_TIMEOUT
198214
)

0 commit comments

Comments
 (0)