Skip to content

Commit

Permalink
Merge a71bccf into 5c0b193
Browse files Browse the repository at this point in the history
  • Loading branch information
smulesoft committed Apr 20, 2017
2 parents 5c0b193 + a71bccf commit c52fa1c
Show file tree
Hide file tree
Showing 10 changed files with 392 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -35,6 +35,7 @@
"babel-preset-es2015-loose": "^8.0.0",
"chai": "^3.5.0",
"coveralls": "~2.11.1",
"dockerode": "^2.4.3",
"eslint": "2.2.0",
"eslint-plugin-import": "^1.8.0",
"istanbul": "^0.4.5",
Expand Down
3 changes: 3 additions & 0 deletions src/dialects/postgres/index.js
Expand Up @@ -111,6 +111,9 @@ assign(Client_PG.prototype, {
connection.on('error', (err) => {
connection.__knex__disposed = err
})
connection.on('end', (err) => {
connection.__knex__disposed = err || 'Connection ended unexpectedly';
})
if (!client.version) {
return client.checkVersion(connection).then(function(version) {
client.version = version;
Expand Down
29 changes: 29 additions & 0 deletions test/docker/docker.js
@@ -0,0 +1,29 @@
const _ = require('lodash');
const $Docker = require('dockerode');

function Docker() {
this.dockerAPI = new $Docker({ socketPath: '/var/run/docker.sock' });
}

/**
* @param {String} name
* @param {String} image
* @param {Object} options
* @returns Promise<Object>
*/
Docker.prototype.createContainer = function (name, image, options) {
return this.dockerAPI.createContainer({
name: name,
Image: image,
AttachStdin: _.get(options, 'AttachStdin', false),
AttachStdout: _.get(options, 'AttachStdout', true),
AttachStderr: _.get(options, 'AttachStderr', true),
Tty: _.get(options, 'Tty', true),
OpenStdin: _.get(options, 'OpenStdin', false),
StdinOnce: _.get(options, 'StdinOnce', false),
Env: _.get(options, 'Env'),
PortBindings: _.get(options, 'PortBindings')
});
}

module.exports = Docker;
58 changes: 58 additions & 0 deletions test/docker/dockerContainer.js
@@ -0,0 +1,58 @@
'use strict';

const Promise = require('bluebird');
const _ = require('lodash');

function DockerContainer(docker, name, image, options) {
this.container = docker.createContainer(name, image, options);
}

/**
* @returns {Promise}
*/
DockerContainer.prototype.start = function () {
return this.container.then((c) =>
c.start().then(() => {
console.log(`#~ Started container ${c.id}`);
return this.waitReady();
})
)
};

/**
* @returns {Promise}
*/
DockerContainer.prototype.waitReady = function () {
return Promise.resolve(this);
}

/**
* @returns {Promise}
*/
DockerContainer.prototype.stop = function () {
return this.container.then((c) =>
c.stop().then(() =>
console.log(`#~ Stopped container ${c.id}`)
)
.catch((err) => {
if (err.statusCode !== 304) {
throw err;
}
})
);
}

/**
* @returns {Promise}
*/
DockerContainer.prototype.destroy = function () {
return this.stop().then(() =>
this.container.then((c) =>
c.remove().then(() =>
console.log(`#~ Removed container ${c.id}`)
)
)
);
}

module.exports = DockerContainer;
21 changes: 21 additions & 0 deletions test/docker/index.js
@@ -0,0 +1,21 @@
'use strict';

var os = require('os');
var proc = require('child_process')
var config = require('../knexfile');
var knex = require('../../knex');
var Promise = require('bluebird');

if (canRunDockerTests()) {
Promise.each(Object.keys(config), function(dialectName) {
if (config[dialectName].docker) {
return require('./reconnect')(config[dialectName], knex);
}
});
}

function canRunDockerTests() {
var isLinux = os.platform() === 'linux';
var hasDocker = proc.execSync('docker -v 1>/dev/null 2>&1 ; echo $?').toString('utf-8') === '0\n';
return isLinux && hasDocker;
}
52 changes: 52 additions & 0 deletions test/docker/mysql/index.js
@@ -0,0 +1,52 @@
'use strict';

const Promise = require('bluebird');
const _ = require('lodash');
const DockerContainer = require('../dockerContainer');

function MySQLContainer(docker, options) {
var name = _.get(options, 'container');
var image = _.get(options, 'image');
var username = _.get(options, 'username');
var password = _.get(options, 'password');
var hostPort = _.get(options, 'hostPort');
DockerContainer.call(this, docker, name, image, {
Env: [ `MYSQL_ROOT_PASSWORD=root` ],
PortBindings: {
'3306/tcp': [{
HostPort: `${hostPort}`
}]
}
});
}

MySQLContainer.prototype = Object.create(DockerContainer.prototype);

/**
* @returns {Promise}
*/
MySQLContainer.prototype.waitReady = function () {
return this.container.then((c) => {
return new Promise((resolve) => {
c.exec({
AttachStdout: true,
Cmd: [
'sh',
'-c',
'until mysqladmin ping -h 127.0.0.1 --silent; do echo "Waiting for mysql readiness" && sleep 2; done'
]
})
.then((exec) =>
exec.start({ Detach: false, Tty: true })
)
.then(({ output }) => {
output.on('data', (data) => {
console.log(data.toString('utf-8').trim());
});
output.on('end', () => resolve(this));
});
})
});
}

module.exports = MySQLContainer;
52 changes: 52 additions & 0 deletions test/docker/postgres/index.js
@@ -0,0 +1,52 @@
'use strict';

const Promise = require('bluebird');
const _ = require('lodash');
const DockerContainer = require('../dockerContainer');

function PostgresContainer(docker, options) {
var name = _.get(options, 'container');
var image = _.get(options, 'image');
var username = _.get(options, 'username');
var password = _.get(options, 'password');
var hostPort = _.get(options, 'hostPort');
DockerContainer.call(this, docker, name, image, {
Env: [`POSTGRES_USER=${username}`, `POSTGRES_PASSWORD=${password}`],
PortBindings: {
'5432/tcp': [{
HostPort: `${hostPort}`
}]
}
});
}

PostgresContainer.prototype = Object.create(DockerContainer.prototype);

/**
* @returns {Promise}
*/
PostgresContainer.prototype.waitReady = function () {
return this.container.then((c) => {
return new Promise((resolve) => {
c.exec({
AttachStdout: true,
Cmd: [
'sh',
'-c',
'until pg_isready; do sleep 1; done'
]
})
.then((exec) =>
exec.start({ Detach: false, Tty: true })
)
.then(({ output }) => {
output.on('data', function (data) {
console.log(data.toString('utf-8').trim());
});
output.on('end', () => resolve(this));
});
})
});
}

module.exports = PostgresContainer;
147 changes: 147 additions & 0 deletions test/docker/reconnect.js
@@ -0,0 +1,147 @@
/*global afterEach, before, expect, describe, it, testPromise*/
'use strict';

var Docker = require('./docker');
var Promise = testPromise;

module.exports = function(config, knex) {

var dockerConf = config.docker;
var ContainerClass = require(dockerConf.factory);

/**
* Make sure the connections in the connection pool are not
* evicted on timeout, they should only be evicted on error.
*/
var EVICTION_RUN_INTERVAL_MILLIS = dockerConf.timeout;
var IDLE_TIMEOUT_MILLIS = dockerConf.timeout;
var ACQUIRE_CONNECTION_TIMEOUT = 10 * 1000;
var ACQUIRE_TIMEOUT_MILLIS = 10 * 1000;

var docker;
var connectionPool;
var container;


describe('using database as a docker container', function () {

this.timeout(dockerConf.timeout);

before(function () {
docker = new Docker();
});

afterEach(function () {
return sequencedPromise(
() => console.log('>> Destroying container'),
() => container.destroy(),
() => console.log('>> Destroying pool'),
() => connectionPool.destroy(),
() => console.log('>> Destroyed all')
);
});

describe('start container and wait until it is ready', function () {

beforeEach(function () {
container = new ContainerClass(docker, dockerConf);
return container.start().then(() => waitReadyForQueries());
});

describe('initialize connection pool', function () {
beforeEach(function () {
connectionPool = createPool();
});

it('connection pool can query', function () {
return testQuery(connectionPool);
});

describe('stop db-container and expect queries to fail', function () {

beforeEach(function () {
return container.stop();
});

it('connection pool can not query x10', function () {
var promises = [];
for (var i = 0; i < 10; i += 1) {
promises.push(
testQuery(connectionPool)
.then(() => { throw new Error('Failure expected'); })
.catch((err) => expect(err.message).to.not.equal('Failure expected'))
);
}
return Promise.all(promises);
});

describe('restart db-container and keep using connection pool', function () {
beforeEach(function () {
return container.start().then(() => waitReadyForQueries());
});

it('connection pool can query x10', function () {
var promises = [];
for (var i = 0; i < 10; i += 1) {
promises.push(testQuery(connectionPool));
}
return Promise.all(promises);
});
});
});
});
})
});

function testQuery(pool) {
return pool.raw(`SELECT 10 as ten`).then((result) => {
expect(result.rows || result[0]).to.deep.equal([{ ten: 10 }]);
});
}

function sequencedPromise(...blocks) {
const base = Promise.resolve(true);
const order = (prev, block) => prev.then(() => block());
return blocks.reduce(order, base);
}

function createPool() {
return knex({
// debug: true,
client: dockerConf.client,
acquireConnectionTimeout: ACQUIRE_CONNECTION_TIMEOUT,
pool: {
min: 7,
max: 7,
idleTimeoutMillis: IDLE_TIMEOUT_MILLIS,
acquireTimeoutMillis: ACQUIRE_TIMEOUT_MILLIS,
evictionRunIntervalMillis: EVICTION_RUN_INTERVAL_MILLIS
},
connection: {
database: dockerConf.database,
port: dockerConf.hostPort,
user: dockerConf.username,
password: dockerConf.password,
host: '127.0.0.1'
}
});
}

function waitReadyForQueries(attempt = 0) {
return new Promise(function (resolve, reject) {
console.log(`#~ Waiting to be ready for queries #${attempt}`);
var pool = createPool();
pool.raw('SELECT 1 as one')
.then(() => pool.destroy().then(resolve))
.catch((a) => {
pool.destroy().then(() => {
if (attempt < 20) {
setTimeout(() => resolve(waitReadyForQueries(attempt + 1)), 1000);
} else {
reject(attempt);
}
})
});
});
}
};
5 changes: 5 additions & 0 deletions test/index.js
Expand Up @@ -43,3 +43,8 @@ describe('Integration Tests', function() {
this.timeout(process.env.KNEX_TEST_TIMEOUT || 5000);
require('./integration')
})

describe('Docker Integration Tests', function() {
this.timeout(process.env.KNEX_TEST_TIMEOUT || 15000);
require('./docker')
})

0 comments on commit c52fa1c

Please sign in to comment.