Skip to content

Commit

Permalink
feat: add connection manager to manage database connections
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jan 12, 2020
1 parent 76d309d commit edf5d80
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 41 deletions.
50 changes: 38 additions & 12 deletions adonis-typings/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
declare module '@ioc:Adonis/Addons/Database' {
import * as knex from 'knex'
import { Pool } from 'tarn'
import { EventEmitter } from 'events'

import { DatabaseQueryBuilderContract } from '@ioc:Adonis/Addons/DatabaseQueryBuilder'

Expand Down Expand Up @@ -190,32 +191,57 @@ declare module '@ioc:Adonis/Addons/Database' {
*/
export type DatabaseConfigContract = { connection: string } & { [key: string]: ConnectionConfigContract }

/**
* The shape of a connection within the connection manager
*/
type ConnectionManagerConnectionNode = {
name: string,
config: ConnectionConfigContract,
connection?: ConnectionContract,
state: 'idle' | 'open' | 'closed',
}

/**
* Connection manager to manage one or more database
* connections.
*/
export interface ConnectionManagerContract {
connections: Map<string, ConnectionContract>
add (connectionName: string, config: ConnectionConfigContract): void,
connect (connectionName: string): void,
get (connectionName: string): ConnectionContract,
has (connectionName: string): boolean,
close (connectionName: string): Promise<void>,
closeAll (): Promise<void>,
export interface ConnectionManagerContract extends EventEmitter {
connections: Map<string, ConnectionManagerConnectionNode>

on (event: 'connect', callback: (connection: ConnectionContract) => void)
on (event: 'disconnect', callback: (connection: ConnectionContract) => void)

add (connectionName: string, config: ConnectionConfigContract): void
connect (connectionName: string): void
get (connectionName: string): ConnectionManagerConnectionNode | undefined
has (connectionName: string): boolean
isConnected (connectionName: string): boolean

close (connectionName: string, release?: boolean): Promise<void>
closeAll (release?: boolean): Promise<void>
release (connectionName: string): Promise<void>
}

/**
* Connection represents a single knex instance with inbuilt
* pooling capabilities.
*/
export interface ConnectionContract {
export interface ConnectionContract extends EventEmitter {
client?: knex,
pool: null | Pool<any>,
name: string,
config: ConnectionConfigContract,
readonly EVENTS: ['open', 'close', 'close:error'],
open (): void,
close (): void,

/**
* List of emitted events
*/
on (event: 'connect', callback: (connection: ConnectionContract) => void)
on (event: 'error', callback: (connection: ConnectionContract, error: Error) => void)
on (event: 'disconnect', callback: (connection: ConnectionContract) => void)
on (event: 'disconnect:error', callback: (connection: ConnectionContract, error: Error) => void)

connect (): void,
disconnect (): Promise<void>,
}

export interface DatabaseContract {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"homepage": "https://github.com/adonisjs/adonis-lucid#readme",
"dependencies": {
"@poppinss/utils": "^1.0.4",
"knex": "^0.19.1",
"ts-essentials": "^3.0.0",
"utility-types": "^3.7.0"
Expand Down
19 changes: 7 additions & 12 deletions src/Connection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ export class Connection extends EventEmitter implements ConnectionContract {
*/
public client?: knex

/**
* List of events emitted by this class
*/
public readonly EVENTS: ['open', 'close', 'close:error']

constructor (public name: string, public config: ConnectionConfigContract) {
super()
}
Expand All @@ -46,7 +41,7 @@ export class Connection extends EventEmitter implements ConnectionContract {
* when `min` resources inside the pool are set to `0`.
*/
if (this.pool!.numFree() === 0 && this.pool!.numUsed() === 0) {
this.close()
this.disconnect()
}
})

Expand All @@ -56,7 +51,7 @@ export class Connection extends EventEmitter implements ConnectionContract {
*/
this.pool!.on('poolDestroySuccess', () => {
this.client = undefined
this.emit('close')
this.emit('disconnect', this)
this.removeAllListeners()
})
}
Expand All @@ -71,13 +66,13 @@ export class Connection extends EventEmitter implements ConnectionContract {
/**
* Opens the connection by creating knex instance
*/
public open () {
public connect () {
try {
this.client = knex(this.config)
this._monitorPoolResources()
this.emit('open')
this.emit('connect', this)
} catch (error) {
this.emit('error', error)
this.emit('error', error, this)
throw error
}
}
Expand All @@ -89,12 +84,12 @@ export class Connection extends EventEmitter implements ConnectionContract {
* In case of error this method will emit `close:error` event followed
* by the `close` event.
*/
public async close (): Promise<void> {
public async disconnect (): Promise<void> {
if (this.client) {
try {
await this.client!.destroy()
} catch (error) {
this.emit('close:error', error)
this.emit('disconnect:error', error, this)
}
}
}
Expand Down
188 changes: 188 additions & 0 deletions src/ConnectionManager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/// <reference path="../../adonis-typings/database.ts" />

import { EventEmitter } from 'events'
import { Exception } from '@poppinss/utils'

import {
ConnectionConfigContract,
ConnectionContract,
ConnectionManagerContract,
} from '@ioc:Adonis/Addons/Database'

import { Connection } from '../Connection'

/**
* Connection class manages a given database connection. Internally it uses
* knex to build the database connection with appropriate database
* driver.
*/
export class ConnectionManager extends EventEmitter implements ConnectionManagerContract {
public connections: ConnectionManagerContract['connections'] = new Map()

/**
* Monitors a given connection by listening for lifecycle events
*/
private _monitorConnection (connection: ConnectionContract) {
/**
* Listens for disconnect to set the connection state and cleanup
* memory
*/
connection.on('disconnect', ($connection) => {
const internalConnection = this.get($connection.name)

/**
* This will be false, when connection was released at the
* time of closing
*/
if (!internalConnection) {
return
}

this.emit('disconnect', $connection)
delete internalConnection.connection
internalConnection.state = 'closed'
})

/**
* Listens for connect to set the connection state to open
*/
connection.on('connect', ($connection) => {
const internalConnection = this.get($connection.name)
if (!internalConnection) {
return
}

this.emit('connect', $connection)
internalConnection.state = 'open'
})
}

/**
* Add a named connection with it's configuration. Make sure to call `connect`
* before using the connection to make database queries.
*/
public add (connectionName: string, config: ConnectionConfigContract): void {
/**
* Raise an exception when someone is trying to re-add the same connection. We
* should not silently avoid this scanerio, since there is a valid use case
* in which the config has been changed and someone wants to re-add the
* connection with new config. In that case, they must
*
* 1. Close and release the old connection
* 2. Then add the new connection
*/
if (this.isConnected(connectionName)) {
throw new Exception(
`Attempt to add duplicate connection ${connectionName} failed`,
500,
'E_DUPLICATE_DB_CONNECTION',
)
}

this.connections.set(connectionName, {
name: connectionName,
config: config,
state: 'idle',
})
}

/**
* Connect to the database using config for a given named connection
*/
public connect (connectionName: string): void {
const connection = this.connections.get(connectionName)
if (!connection) {
throw new Exception(
`Cannot connect to unregisted connection ${connectionName}`,
500,
'E_MISSING_DB_CONNECTION_CONFIG',
)
}

/**
* Do not do anything when `connection` property already exists, since it will
* always be set to `undefined` for a closed connection
*/
if (connection.connection) {
return
}

/**
* Create a new connection and monitor it's state
*/
connection.connection = new Connection(connection.name, connection.config)
this._monitorConnection(connection.connection)
connection.connection.connect()
}

/**
* Returns the connection node for a given named connection
*/
public get (connectionName: string) {
return this.connections.get(connectionName)
}

/**
* Returns a boolean telling if we have connection details for
* a given named connection. This method doesn't tell if
* connection is connected or not.
*/
public has (connectionName: string) {
return this.connections.has(connectionName)
}

/**
* Returns a boolean telling if connection has been established
* with the database or not
*/
public isConnected (connectionName: string) {
if (!this.has(connectionName)) {
return false
}

const connection = this.get(connectionName)!
return (!!connection.connection && connection.state === 'open')
}

/**
* Closes a given connection and can optionally release it from the
* tracking list
*/
public async close (connectioName: string, release: boolean = false) {
if (this.isConnected(connectioName)) {
await this.get(connectioName)!.connection!.disconnect()
}

if (release) {
await this.release(connectioName)
}
}

/**
* Close all tracked connections
*/
public async closeAll (release: boolean = false) {
await Promise.all(Array.from(this.connections.keys()).map((name) => this.close(name, release)))
}

/**
* Release a connection. This will disconnect the connection
* and will delete it from internal list
*/
public async release (connectionName: string) {
if (this.isConnected(connectionName)) {
await this.close(connectionName, true)
} else {
this.connections.delete(connectionName)
}
}
}
Loading

0 comments on commit edf5d80

Please sign in to comment.