Skip to content

Commit

Permalink
fix fun local start could not receive logs bug (#166)
Browse files Browse the repository at this point in the history
* fix fun local start could not receive logs bug

* fix initializer when using http trigger

* add force close express server

* optimize force server close

* process runtime exception when http invoke

* add runtime error handler

* check container exist before stopping container
  • Loading branch information
tanhe123 authored Mar 13, 2019
1 parent df10ff8 commit 88e5e8c
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 40 deletions.
8 changes: 6 additions & 2 deletions examples/local_http/nodejs8/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
var getRawBody = require('raw-body')

module.exports.handler = function (request, response, context) {

module.exports.initializer = function(context, callback) {
console.log("initializer invoked");
callback(null, '');
}

module.exports.handler = function (request, response, context) {
// get request body
getRawBody(request, function (err, body) {
var respBody = {
Expand Down
1 change: 1 addition & 0 deletions examples/local_http/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Resources:
CodeUri: nodejs8/
Description: 'http trigger demo with nodejs8!'
Runtime: nodejs8
Initializer: index.initializer
Events:
http-test:
Type: HTTP
Expand Down
33 changes: 27 additions & 6 deletions lib/commands/local/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,54 @@ const definition = require('../../definition');
const { getDebugPort, getDebugIde } = require('../../debug');

const serverPort = 8000;
const SERVER_CLOSE_TIMEOUT = 5000;

function registerSigintForExpress(server) {
var sockets = {}, nextSocketId = 0;

// close express server
// https://stackoverflow.com/questions/14626636/how-do-i-shutdown-a-node-js-https-server-immediately/14636625#14636625
server.on('connection', function(socket) {
server.on('connection', function (socket) {
let socketId = nextSocketId;
sockets[socketId] = socket;
socket.on('close', function() {
socket.on('close', function () {
delete sockets[socketId];
});
});

process.on('SIGINT', () => {
server.close();
process.once('SIGINT', () => {

console.log('begin to close server');

// force close if gracefully closing failed
// https://stackoverflow.com/a/36830072/6602338
const serverCloseTimeout = setTimeout(() => {
console.log('server close timeout, force to close server');

server.emit('close');

// if force close failed, exit directly
setTimeout(() => {
process.exit(-1);
}, SERVER_CLOSE_TIMEOUT);

}, SERVER_CLOSE_TIMEOUT);

// gracefully close server
server.close(() => {
clearTimeout(serverCloseTimeout);
});

for (let socketId in sockets) {
if (!{}.hasOwnProperty.call(sockets, socketId)) {continue;}
if (!{}.hasOwnProperty.call(sockets, socketId)) { continue; }

sockets[socketId].destroy();
}
});
}

function startExpress(app) {

const server = app.listen(serverPort, function () {
console.log(`function compute app listening on port ${serverPort}!`);
console.log();
Expand Down
43 changes: 38 additions & 5 deletions lib/docker.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,22 @@ function waitingForContainerStopped() {
const jobs = [];

for (let container of containers) {
console.log(`stopping container ${container}`);
jobs.push(docker.getContainer(container).stop());
try {
const c = docker.getContainer(container);

await c.inspect();

console.log(`stopping container ${container}`);

jobs.push(c.stop());
} catch(error) {
debug('get container instance error, ignore container to stop, error is', error);
}
}

try {
await Promise.all(jobs);
console.log('all containers stopped');
} catch (error) {
console.error(error);
process.exit(-1);
Expand Down Expand Up @@ -176,7 +186,7 @@ async function imageExist(imageName) {
return images.length > 0;
}

function generateDockerCmd(functionProps, httpMode) {
function generateDockerCmd(functionProps, httpMode, invokeInitializer = true) {
const cmd = ['-h', functionProps.Handler];

// always pass event using stdin mode
Expand All @@ -188,7 +198,7 @@ function generateDockerCmd(functionProps, httpMode) {

const initializer = functionProps.Initializer;

if (initializer) {
if (initializer && invokeInitializer) {
cmd.push('-i', initializer);
}

Expand Down Expand Up @@ -396,7 +406,9 @@ function resolveDockerUser(nasConfig) {
return `${uid}:${gid}`;
}

async function startContainer(opts) {
// outputStream, errorStream used for http invoke
// because agent is started when container running and exec could not receive related logs
async function startContainer(opts, outputStream, errorStream) {

const container = await docker.createContainer(opts);

Expand All @@ -408,6 +420,27 @@ async function startContainer(opts) {
console.error(err);
}

const logs = outputStream || errorStream;

if (logs) {
if (!outputStream) {
outputStream = devnull();
}

if (!errorStream) {
errorStream = devnull();
}

// dockerode bugs on windows. attach could not receive output and error, must use logs
const logStream = await container.logs({
stdout: true,
stderr: true,
follow: true
});

container.modem.demuxStream(logStream, outputStream, errorStream);
}

return {
stop: async () => {
await container.stop();
Expand Down
70 changes: 48 additions & 22 deletions lib/local/http-invoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class HttpInvoke extends Invoke {

this.isAnonymous = authType === 'ANONYMOUS';
this.endpointPrefix = endpointPrefix;
this._invokeInitializer = true;
}

async _disableRunner(evt, name) {
Expand All @@ -41,6 +42,7 @@ class HttpInvoke extends Invoke {

this.runner = null;
this.containerName = docker.generateRamdomContainerName();
this._invokeInitializer = true;

console.log('reloading success, stop old container background...');

Expand Down Expand Up @@ -70,8 +72,6 @@ class HttpInvoke extends Invoke {
if (!this.runner) {
debug('acquire invoke lock success, ready to create runner');

await docker.pullImageIfNeed(this.imageName);

if (!this.watcher) {
watch(this.codeMount.Source, { recursive: true, persistent: false }, (evt, name) => {
if (this.runner) {
Expand All @@ -82,17 +82,7 @@ class HttpInvoke extends Invoke {
});
}

const envs = await docker.generateDockerEnvs(this.functionProps, this.debugPort, null);

const opts = await dockerOpts.generateLocalStartRunOpts(this.runtime,
this.containerName,
this.mounts,
['--server'],
this.debugPort,
envs,
this.dockerUser);

this.runner = await startContainer(opts);
await this._generateRunner();
} else {
debug('acquire invoke lock success, but runner already created, skipping...');
}
Expand All @@ -101,6 +91,21 @@ class HttpInvoke extends Invoke {
}
}

async _generateRunner() {

const envs = await docker.generateDockerEnvs(this.functionProps, this.debugPort, null);

const opts = await dockerOpts.generateLocalStartRunOpts(this.runtime,
this.containerName,
this.mounts,
['--server'],
this.debugPort,
envs,
this.dockerUser);

this.runner = await startContainer(opts, process.stdout, process.stderr);
}

async doInvoke(req, res) {
// only one invoke can be processed
await lock.acquire('invoke', async () => {
Expand Down Expand Up @@ -137,7 +142,7 @@ class HttpInvoke extends Invoke {
// reuse container
debug('http doInvoke, acquire invoke lock');

const cmd = [dockerOpts.resolveMockScript(this.runtime), ...docker.generateDockerCmd(this.functionProps, true)];
const cmd = [dockerOpts.resolveMockScript(this.runtime), ...docker.generateDockerCmd(this.functionProps, true, this._invokeInitializer)];

debug(`http doInvoke, cmd is : ${cmd}`);

Expand All @@ -146,14 +151,35 @@ class HttpInvoke extends Invoke {
if (!await validateSignature(req, res, req.method)) { return; }
}

await this.runner.exec(cmd, {
env: envs,
event,
outputStream,
errorStream,
verbose: true
});

try {
await this.runner.exec(cmd, {
env: envs,
event,
outputStream,
errorStream,
verbose: true
});

this._invokeInitializer = false;
} catch(error) {
// errors for runtime error
// for example, when using nodejs, use response.send(new Error('haha')) will lead to runtime error
// and container will auto exit, exec will receive no message
res.status(500);
res.setHeader('Content-Type', 'application/json');

res.send({
'errorMessage': `Process exited unexpectedly before completing request`
});

// for next invoke
this.runner = null;
this.containerName = docker.generateRamdomContainerName();

console.error(error);
return ;
}

debug('http doInvoke exec end, begin to response');
}

Expand Down
2 changes: 1 addition & 1 deletion lib/package/ignore.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const parser = require('git-ignore-parser'),
fs = require('fs'),
path = require('path');

const ignoredFile = ['.git', '.svn', '.env'];
const ignoredFile = ['.git', '.svn', '.env', '.fun/nas'];

module.exports = function (baseDir) {

Expand Down
19 changes: 18 additions & 1 deletion test/docker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ describe('test generateDockerCmd', () => {
'3'
]);
});

it('test generate docker http cmd without initializer', () => {
const cmd = docker.generateDockerCmd(functionProps, true, false);

expect(cmd).to.eql([
'-h',
'index.handler',
'--stdin',
'--http',
'--initializationTimeout',
'3'
]);
});
});


Expand Down Expand Up @@ -255,7 +268,10 @@ describe('test docker run', async () => {

sandbox.stub(DockerCli.prototype, 'pull').resolves({});
sandbox.stub(DockerCli.prototype, 'run').resolves({});
sandbox.stub(DockerCli.prototype, 'getContainer').returns({ 'stop': sandbox.stub() });
sandbox.stub(DockerCli.prototype, 'getContainer').returns({
'stop': sandbox.stub(),
'inspect': sandbox.stub().resolves({})
});

streamMock = {
'write': sandbox.stub(),
Expand Down Expand Up @@ -384,6 +400,7 @@ describe('test docker run', async () => {
await sleep(10);

assert.calledWith(DockerCli.prototype.getContainer, 'containerId');
assert.calledOnce(DockerCli.prototype.getContainer().inspect);
assert.calledOnce(DockerCli.prototype.getContainer().stop);
});
});
Expand Down
13 changes: 10 additions & 3 deletions test/local/http-invoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ describe('test http response', async () => {
let app;
let server;
let restoreProcess;
let httpInvoke;

beforeEach(async () => {

Expand All @@ -125,7 +126,7 @@ def handler(environ, start_response):
`);
});

afterEach(async function () {
afterEach(async () => {
rimraf.sync(projectDir);
restoreProcess();

Expand All @@ -142,7 +143,7 @@ def handler(environ, start_response):
const endpointPrefix = `/2016-08-15/proxy/${serviceName}/${functionName}`;
const endpoint = `${endpointPrefix}*`;

const httpInvoke = new HttpInvoke(serviceName, httptriggerServiceRes,
httpInvoke = new HttpInvoke(serviceName, httptriggerServiceRes,
functionName, httpTriggerFunctionRes, null, null, ymlPath, 'ANONYMOUS', endpointPrefix);

app.get(endpoint, async (req, res) => {
Expand All @@ -157,6 +158,8 @@ def handler(environ, start_response):
const body = await httpx.read(resp, 'utf8');

expect(body).to.contain('Hello world!');

await httpInvoke.runner.stop();
});

it('test http local invoke with authType function with invalid signature', async () => {
Expand All @@ -170,7 +173,7 @@ def handler(environ, start_response):
const endpointPrefix = `/2016-08-15/proxy/${serviceName}/${functionName}`;
const endpoint = `${endpointPrefix}*`;

const httpInvoke = new HttpInvoke(serviceName, httptriggerServiceRes,
httpInvoke = new HttpInvoke(serviceName, httptriggerServiceRes,
functionName, httpTriggerFunctionRes, null, null, ymlPath, 'FUNCTION', endpointPrefix);

app.get(endpoint, async (req, res) => {
Expand All @@ -185,6 +188,8 @@ def handler(environ, start_response):
const body = await httpx.read(resp, 'utf8');

expect(body).to.contain('Signature doesn\'t match, request signature is');

await httpInvoke.runner.stop();
});

it('test http local invoke with authType function with valid signature', async () => {
Expand Down Expand Up @@ -222,5 +227,7 @@ def handler(environ, start_response):
const body = await httpx.read(resp, 'utf8');

expect(body).to.contain('Hello world!');

await httpInvoke.runner.stop();
});
});

0 comments on commit 88e5e8c

Please sign in to comment.