Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
alexpozzi committed May 11, 2021
0 parents commit 605caf7
Show file tree
Hide file tree
Showing 19 changed files with 4,377 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"presets": [
"@babel/preset-env"
]
}
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# ignore all by default
*

!src/
!.babelrc
!package.json
!yarn.lock
14 changes: 14 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Global configurations
LOG_LEVEL=DEBUG

# Queue configurations
QUEUE_REDIS_DSN=redis://redis:6379

# Manager configurations
MANAGER_ENABLED=1
MANAGER_HTTP_SERVER_PORT=8990

# Worker configurations
WORKER_ENABLED=1
WORKER_RENDERER_AUTHORIZED_REQUEST_DOMAINS=front.preprod-knp.i24news.org,api.preprod-knp.i24news.org
WORKER_RENDERER_AUTHORIZED_REQUEST_TYPES=document,script,xhr,fetch,eventsource,websocket
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/build
/node_modules
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
46 changes: 46 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
FROM node:slim as dev

# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-unstable
RUN apt-get update \
&& apt-get install -y wget gnupg \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.1/dumb-init_1.2.1_amd64 /usr/local/bin/dumb-init
RUN chmod +x /usr/local/bin/dumb-init

WORKDIR /app

COPY --chown=1000:1000 src/ src/
COPY --chown=1000:1000 package.json package.json
COPY --chown=1000:1000 yarn.lock yarn.lock
COPY --chown=1000:1000 .babelrc .babelrc

RUN yarn install

USER 1000

ENTRYPOINT ["dumb-init", "--"]
CMD ["yarn", "dev"]

################################################################################

FROM dev as prod

USER root

ENV BABEL_ENV=production
ENV NODE_ENV=production

RUN yarn run build

USER 1000

CMD ["yarn", "start"]
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
IMAGE_NAME = knplabs/server-side-renderer

.PHONY: build
build: .validate-tag
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .

.PHONY: build
push: .validate-tag
docker push ${IMAGE_NAME}:${IMAGE_TAG}

.PHONY: .validate-tag
.validate-tag:
ifeq ($(IMAGE_TAG),)
@echo "You can't build and push without an IMAGE_TAG.\n"
@exit 1
endif
17 changes: 17 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: '3.6'

services:
chromium_pool:
image: knplabs:chromium_pool
env_file: .env
build:
context: .
target: dev
command: 'yarn dev'
ports:
- "8990:8990"
volumes:
- ./:/app

redis:
image: redis:6.2.2-buster
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "chromium-pool",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"bull": "^3.22.4",
"express": "^4.17.1",
"puppeteer": "^9.1.1",
"ramda": "^0.27.1",
"tree-kill": "^1.2.2",
"uuid": "^8.3.2"
},
"devDependencies": {
"@babel/cli": "^7.13.16",
"@babel/core": "^7.14.0",
"@babel/node": "^7.13.13",
"@babel/preset-env": "^7.14.1",
"nodemon": "^2.0.7",
"regenerator-runtime": "^0.13.7"
},
"scripts": {
"dev": "nodemon --watch src/ --exec babel-node src/index.js",
"build": "babel src/ -d build/ --ignore 'src/**/*.test.js'",
"start": "node build/index.js"
}
}
24 changes: 24 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'regenerator-runtime/runtime' // needed by the SSR to be able to execute transpiled generator functions like async/await
import { call, equals, pipe } from 'ramda'
import { default as createLogger, DEFAULT_LEVEL } from './logger'
import setupProcessHandlers from './processHandlers'
import createQueue from './queue'
import initManager from './manager'
import initWorker from './worker'

const logger = createLogger(process.env.LOG_LEVEL || DEFAULT_LEVEL, console)

const queue = createQueue(process.env.QUEUE_REDIS_DSN)

const shouldStartManager = () => equals(1, Number(process.env.MANAGER_ENABLED))

const shouldStartWorker = () => equals(1, Number(process.env.WORKER_ENABLED))

// main :: (Logger, Queue) => _
const main = (logger, queue) => call(pipe(
() => shouldStartManager() && initManager(logger, queue),
() => shouldStartWorker() && initWorker(logger, queue),
() => setupProcessHandlers(logger),
))

main(logger, queue)
76 changes: 76 additions & 0 deletions src/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { __, bind, equals, findIndex, gt, gte, partial, pipe, when, pickAll } from 'ramda'

const LEVEL_ERROR = 'ERROR'
const LEVEL_WARN = 'WARN'
const LEVEL_INFO = 'INFO'
const LEVEL_DEBUG = 'DEBUG'

const levels = [
LEVEL_ERROR,
LEVEL_WARN,
LEVEL_INFO,
LEVEL_DEBUG,
]

// resolveLogLevelIndex :: String -> Number
const resolveLogLevelIndex = pipe(
level => findIndex(equals(level), levels),
when(gt(0), levels.length),
)

// loggerHead :: (String, String) -> Boolean
const shouldPrintLog = (loggerLevel, logLevel) => gte(
resolveLogLevelIndex(loggerLevel),
resolveLogLevelIndex(logLevel),
)

// loggerHead :: String-> String
const loggerHead = type => `[${(new Date()).toISOString()}] ${type.toUpperCase()}:`

// error :: (String, Output) -> Function
const error = (level, output) => when(
level => shouldPrintLog(level, LEVEL_ERROR),
() => partial(
bind(output.error, output),
[loggerHead(LEVEL_ERROR)],
),
)(level)

// warn :: (String, Output) -> Function
const warn = (level, output) => when(
level => shouldPrintLog(level, LEVEL_WARN),
() => partial(
bind(output.warn, output),
[loggerHead(LEVEL_WARN)],
),
)(level)

// info :: (String, Output) -> Function
const info = (level, output) => when(
level => shouldPrintLog(level, LEVEL_INFO),
() => partial(
bind(output.info, output),
[loggerHead(LEVEL_INFO)],
),
)(level)

// debug :: (String, Output) -> Function
const debug = (level, output) => when(
level => shouldPrintLog(level, LEVEL_DEBUG),
() => partial(
bind(output.log, output),
[loggerHead(LEVEL_DEBUG)],
),
)(level)

export const DEFAULT_LEVEL = LEVEL_DEBUG

export const formatException = e => JSON.stringify(pickAll(['code', 'message', 'stack'], e))

// createLogger :: (String, Output) -> Logger
export default (level, output) => ({
error: error(level, output),
info: info(level, output),
debug: debug(level, output),
warn: warn(level, output),
})
13 changes: 13 additions & 0 deletions src/manager/http-server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { pipe } from 'ramda'
import express from 'express'
import attachRenderMiddleware from './middlewares/render'

// createHttpServer => (Logger, Queue) -> HttpServer
export default (logger, queue, requestRegistry) => pipe(
attachRenderMiddleware(logger, queue, requestRegistry),
app => app.listen(
Number(process.env.MANAGER_HTTP_SERVER_PORT) || 8990,
process.env.MANAGER_HTTP_SERVER_HOST || '0.0.0.0',
() => logger.info('Manager http server started.'),
)
)(express())
20 changes: 20 additions & 0 deletions src/manager/http-server/middlewares/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { call, complement, compose, ifElse, isNil, path, pipe } from 'ramda'
import { DEFAULT_JOB_OPTIONS } from '../../../queue'

export default (logger, queue, requestRegistry) => app =>
app.get('/render', (req, res, next) => call(pipe(
() => logger.info(`Render request for url "${req.query.url}" started.`),
ifElse(
() => compose(complement(isNil), path(['query', 'url']))(req),
pipe(
() => requestRegistry.add(req, res, next),
jobId => queue.add({
url: req.query.url
}, {
...DEFAULT_JOB_OPTIONS,
jobId,
}),
),
() => res.status(400).end('Missing url query parameter.')
),
)))
29 changes: 29 additions & 0 deletions src/manager/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { call, juxt, pipe, tap, when } from 'ramda'
import initHttpServer from './http-server'
import createRequestRegistry from './requestRegistry'

const onJobCompleted = (logger, requestRegistry) => (jobId, result) => when(
jobId => requestRegistry.has(jobId),
juxt([
jobId => requestRegistry.complete(jobId, JSON.parse(result)),
jobId => logger.info(`Completed job "${jobId}"`),
]),
)(jobId)

const onJobFailed = (logger, requestRegistry) => (jobId, error) => when(
jobId => requestRegistry.has(jobId),
juxt([
jobId => requestRegistry.fail(jobId, error),
jobId => logger.error(`Job "${jobId}" has failed. ${error}.`),
]),
)(jobId)

// initManager :: (Logger, Queue) -> _
export default (logger, queue) => call(pipe(
() => logger.info('Initializing manager.'),
() => createRequestRegistry(),
tap(requestRegistry => queue.on('global:completed', onJobCompleted(logger, requestRegistry))),
tap(requestRegistry => queue.on('global:failed', onJobFailed(logger, requestRegistry))),
requestRegistry => initHttpServer(logger, queue, requestRegistry),
() => logger.info('Manager initialized.'),
))
31 changes: 31 additions & 0 deletions src/manager/requestRegistry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { v4 as uuidv4 } from 'uuid'

export default () => ({
_requests: {},

has: function (id) {
return this._requests.hasOwnProperty(id)
},

add: function (req, res, next) {
const id = uuidv4()

this._requests[id] = { req, res, next }

return id
},

complete: function (id, result) {
if (this.has(id)) {
this._requests[id].res.send(result)
delete this._requests[id]
}
},

fail: function (id, error) {
if (this.has(id)) {
this._requests[id].res.status(500).end()
delete this._requests[id]
}
},
})
Loading

0 comments on commit 605caf7

Please sign in to comment.