Skip to content

Commit

Permalink
feat: exit immediately when fatal error occurs
Browse files Browse the repository at this point in the history
  • Loading branch information
pimlie committed Jan 30, 2019
1 parent 2ede4c2 commit e3a1176
Show file tree
Hide file tree
Showing 25 changed files with 183 additions and 289 deletions.
47 changes: 22 additions & 25 deletions bin/nuxt-generate → bin/nuxt-generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ if (cluster.isMaster) {
(should be a JSON string or queryString)
-q, --quiet Decrease verbosity (repeat to decrease more)
-v, --verbose Increase verbosity (repeat to increase more)
--fail-on-page-error Immediately exit when a page throws an unhandled error
-w, --workers [NUM] How many workers should be started
(default: # cpus)
-wc [NUM], How many routes should be sent to
--worker-concurrency [NUM] a worker per iteration
-f, --fail-on-error Exit with code 1 if a page throws an unhandled error
`, {
flags: {
Expand Down Expand Up @@ -64,17 +64,15 @@ if (cluster.isMaster) {
default: false,
alias: 'wc'
},
'fail-on-error': {
'fail-on-page-error': {
type: 'boolean',
default: false,
alias: 'f'
default: false
}
}
})

const resolve = require('path').resolve
const existsSync = require('fs').existsSync
const util = require('util')
const store = new (require('data-store'))('nuxt-generate-cluster')

const rootDir = resolve(cli.input[0] || '.')
Expand Down Expand Up @@ -139,11 +137,26 @@ if (cluster.isMaster) {

const { Master } = require('..')

// require consola after importing Master
const consola = require('consola')
consola.addReporter({
log(logObj) {
if (logObj.type === 'fatal') {
// Exit immediately on fatal error
// the error itself is already printed by the other reporter
// because logging happens sync and this reporter is added
// after the normal one
process.exit(1)
}
}
})

storeTime('lastStarted')
const master = new Master(options, {
adjustLogLevel: countFlags('v') - countFlags('q'),
workerCount: cli.flags.workers,
workerConcurrency: cli.flags.workerConcurrency
workerConcurrency: cli.flags.workerConcurrency,
failOnPageError: cli.flags.failOnPageError
})

master.hook('built', (params) => {
Expand All @@ -153,7 +166,7 @@ if (cluster.isMaster) {
master.hook('done', ({ duration, errors, workerInfo }) => {
storeTime('lastFinished')

global.consola.log(`HTML Files generated in ${duration}s`)
consola.log(`HTML Files generated in ${duration}s`)

if (errors.length) {
const report = errors.map(({ type, route, error }) => {
Expand All @@ -164,29 +177,13 @@ if (cluster.isMaster) {
return `Route: '${route}' thrown an error: \n` + JSON.stringify(error)
}
})
global.consola.error('==== Error report ==== \n' + report.join('\n\n'))
}

const workersWithErrors = Object.values(workerInfo).filter(i => i.code !== 0)
if (workersWithErrors.length > 0) {
global.consola.fatal(`${workersWithErrors.length} workers failed:\n${util.inspect(workersWithErrors)}`)
process.exit(1)
}

const unhandledErrors = errors.filter(e => e.type === 'unhandled')
if (unhandledErrors.length > 0 && cli.flags.failOnError) {
global.consola.fatal(`There were ${unhandledErrors.length} unhandled page rendering errors.`)
process.exit(1)
consola.error('==== Error report ==== \n' + report.join('\n\n'))
}
})

params = Object.assign({}, store.data || {}, params || {})
master.run({ build: cli.flags.build, params })
} else {
const { Worker } = require('..')

const options = JSON.parse(process.env.options)

const worker = new Worker(options)
worker.run()
Worker.start()
}
12 changes: 10 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
const fs = require('fs')
const path = require('path')

if (fs.existsSync(path.resolve(__dirname, '.babelrc'))) {
if (fs.existsSync(path.resolve(__dirname, '.git'))) {
// Use esm version when using linked repository to prevent builds
const requireModule = require('esm')(module, {})
const requireModule = require('esm')(module, {
cache: false,
cjs: {
cache: true,
vars: true,
namedExports: true
}
})

module.exports = requireModule('./lib/index.js').default
} else {
// Use production bundle by default
Expand Down
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ module.exports = {
collectCoverageFrom: [
'lib/**'
],
setupTestFrameworkScriptFile: './test/utils/setup',
setupFilesAfterEnv: ['./test/utils/setup'],
testPathIgnorePatterns: ['test/fixtures/.*/.*?/'],
transformIgnorePatterns: ['<rootDir>/node_modules/'],
moduleFileExtensions: ['js', 'mjs', 'json'],
expand: true,
forceExit: true
forceExit: false
// https://github.com/facebook/jest/pull/6747 fix warning here
// But its performance overhead is pretty bad (30+%).
// detectOpenHandles: true
Expand Down
2 changes: 1 addition & 1 deletion lib/async/master.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default class Master extends Messaging(GenerateMaster) {
async startWorkers() {
for (let i = await this.watchdog.countAlive(); i < this.workerCount; i++) {
this.lastWorkerId++
const worker = new Worker(this.options, this.lastWorkerId)
const worker = new Worker(this.options, {}, this.lastWorkerId)
this.workers.push(worker)
this.watchdog.addWorker(worker.id)

Expand Down
2 changes: 1 addition & 1 deletion lib/async/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Messaging } from './mixins'
const debug = Debug('nuxt:worker')

export default class Worker extends Messaging(GenerateWorker) {
constructor(options, id) {
constructor(options, cliOptions = {}, id) {
super(options)

this.setId(id)
Expand Down
32 changes: 26 additions & 6 deletions lib/cluster/master.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Master as GenerateMaster, Commands } from '../generate'
import { consola, messaging } from '../utils'

export default class Master extends GenerateMaster {
constructor(options, { workerCount, workerConcurrency, setup, adjustLogLevel } = {}) {
super(options, { adjustLogLevel, workerCount, workerConcurrency })
constructor(options, { workerCount, workerConcurrency, failOnPageError, setup, adjustLogLevel } = {}) {
super(options, { adjustLogLevel, workerCount, failOnPageError, workerConcurrency })

if (setup) {
cluster.setupMaster(setup)
Expand Down Expand Up @@ -78,8 +78,18 @@ export default class Master extends GenerateMaster {
}

async startWorkers() {
for (let i = await this.watchdog.countAlive(); i < this.workerCount; i++) {
cluster.fork({ options: JSON.stringify(this.options) })
// Dont start more workers then there are routes
const maxWorkerCount = Math.min(this.workerCount, this.routes.length)

for (let i = await this.watchdog.countAlive(); i < maxWorkerCount; i++) {
cluster.fork({
args: JSON.stringify({
options: this.options,
cliOptions: {
failOnPageError: this.failOnPageError
}
})
})
}
}

Expand All @@ -96,13 +106,23 @@ export default class Master extends GenerateMaster {
this.watchdog.exitWorker(workerId, { code, signal })

let message = `worker ${workerId} exited`
if (code !== 0) {

let fatal = false
if (code) {
message += ` with status code ${code}`
fatal = true
}

if (signal) {
message += ` by signal ${signal}`
fatal = true
}

if (fatal) {
consola.fatal(message)
} else {
consola.master(message)
}
consola.master(message)

const allDead = await this.watchdog.allDead()
if (allDead) {
Expand Down
16 changes: 14 additions & 2 deletions lib/cluster/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { consola, messaging } from '../utils'
import { Worker as GenerateWorker, Commands } from '../generate'

export default class Worker extends GenerateWorker {
constructor(options) {
super(options)
constructor(options, cliOptions = {}) {
super(options, cliOptions)

if (cluster.isWorker) {
this.setId(cluster.worker.id)
Expand All @@ -19,6 +19,14 @@ export default class Worker extends GenerateWorker {
})
}

static start() {
const args = JSON.parse(process.env.args)

const worker = new Worker(args.options, args.cliOptions)
worker.run()
return worker
}

async init() {
await super.init()

Expand Down Expand Up @@ -79,6 +87,10 @@ export default class Worker extends GenerateWorker {
if (error.type === 'unhandled') {
// convert error stack to a string already, we cant send a stack object to the master process
error.error = { stack: '' + error.error.stack }

if (this.failOnPageError) {
consola.fatal(`Unhandled page error occured for route ${error.route}`)
}
}
return error
})
Expand Down
3 changes: 2 additions & 1 deletion lib/generate/master.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getNuxt, getGenerator } from '../utils/nuxt'
import Watchdog from './watchdog'

export default class Master extends Hookable() {
constructor(options, { workerCount, workerConcurrency, adjustLogLevel }) {
constructor(options, { workerCount, workerConcurrency, failOnPageError, adjustLogLevel }) {
super()

this.options = options
Expand All @@ -15,6 +15,7 @@ export default class Master extends Hookable() {

this.workerCount = parseInt(workerCount)
this.workerConcurrency = parseInt(workerConcurrency)
this.failOnPageError = failOnPageError

if (adjustLogLevel) {
consola.level = Math.max(0, Math.min(consola._maxLevel, consola._defaultLevel + adjustLogLevel))
Expand Down
4 changes: 3 additions & 1 deletion lib/generate/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { Hookable } from '../mixins'
import { getNuxt, getGenerator } from '../utils/nuxt'

export default class Worker extends Hookable() {
constructor(options) {
constructor(options, { failOnPageError } = {}) {
super()
this.options = options
this.id = -1

this.failOnPageError = failOnPageError

if (this.options.__workerLogLevel) {
consola.level = Math.max(0, Math.min(consola._maxLevel, this.options.__workerLogLevel))
}
Expand Down
23 changes: 18 additions & 5 deletions lib/utils/consola.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { isMaster } from 'cluster'
import env from 'std-env'
import figures from 'figures'
import chalk from 'chalk'
import { Consola, Types } from 'consola/dist/consola.cjs.js'
import { BasicReporter, FancyReporter } from './reporters'
import { Consola, BasicReporter, FancyReporter, Types } from 'consola/dist/consola.cjs.js'
import messaging from './messaging'
import { ClusterReporter } from './reporters'

if (typeof global.myConsolaSet === 'undefined') {
// Delete the global.consola set by consola self
Expand Down Expand Up @@ -39,11 +41,22 @@ if (!consola) {
consola._defaultLevel = consola.level
consola._maxLevel = 6

if (env.ci || env.test) {
if (isMaster) {
/* istanbul ignore next */
consola.add(new BasicReporter())
messaging.on('consola', ({ logObj, stream }) => {
logObj.date = new Date(logObj.date)
consola[logObj.type](...logObj.args)
})

if (env.ci || env.test) {
/* istanbul ignore next */
consola.add(new BasicReporter())
} else {
consola.add(new FancyReporter())
}
} else {
consola.add(new FancyReporter())
/* istanbul ignore next */
consola.add(new ClusterReporter())
}

global.myConsolaSet = true
Expand Down
38 changes: 0 additions & 38 deletions lib/utils/reporters/basic.js

This file was deleted.

17 changes: 17 additions & 0 deletions lib/utils/reporters/cluster.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import messaging from '../messaging'

// Consola Reporter
export default class Reporter {
log(logObj, { async } = {}) {
if (logObj.type === 'success' && logObj.args[0].startsWith('Generated ')) {
// Ignore success messages from Nuxt.Generator::generateRoute
return
}

if (global._ngc_log_tag) {
logObj.tag = global._ngc_log_tag
}

messaging.send(null, 'consola', { logObj, stream: { async } })
}
}

0 comments on commit e3a1176

Please sign in to comment.