Skip to content

Commit

Permalink
feat: dummy HTTP API server + Fly deployment (#2)
Browse files Browse the repository at this point in the history
Signed-off-by: Miroslav Bajtoš <[email protected]>
  • Loading branch information
bajtos authored Jan 15, 2024
1 parent 0e03667 commit c1f19b3
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 4 deletions.
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ WORKDIR /app
# Set production environment
ENV NODE_ENV production
ENV SENTRY_ENVIRONMENT production
ENV DOMAIN stats.filspark.com
ENV REQUEST_LOGGING false

#######################################################################
Expand Down
75 changes: 75 additions & 0 deletions bin/spark-stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import http from 'node:http'
import { once } from 'node:events'
import pg from 'pg'
import Sentry from '@sentry/node'
import fs from 'node:fs/promises'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

import { createHandler } from '../lib/handler.js'
import { DATABASE_URL } from '../lib/config.js'

const {
PORT = 8080,
HOST = '127.0.0.1',
SENTRY_ENVIRONMENT = 'development',
REQUEST_LOGGING = 'true'
} = process.env

const pkg = JSON.parse(
await fs.readFile(
join(
dirname(fileURLToPath(import.meta.url)),
'..',
'package.json'
),
'utf8'
)
)

Sentry.init({
dsn: 'https://[email protected]/4506576125427712',
release: pkg.version,
environment: SENTRY_ENVIRONMENT,
tracesSampleRate: 0.1
})

const pgPool = new pg.Pool({
connectionString: DATABASE_URL,
// allow the pool to close all connections and become empty
min: 0,
// this values should correlate with service concurrency hard_limit configured in fly.toml
// and must take into account the connection limit of our PG server, see
// https://fly.io/docs/postgres/managing/configuration-tuning/
max: 100,
// close connections that haven't been used for one second
idleTimeoutMillis: 1000,
// automatically close connections older than 60 seconds
maxLifetimeSeconds: 60
})

pgPool.on('error', err => {
// Prevent crashing the process on idle client errors, the pool will recover
// itself. If all connections are lost, the process will still crash.
// https://github.com/brianc/node-postgres/issues/1324#issuecomment-308778405
console.error('An idle client has experienced an error', err.stack)
})

// Check that we can talk to the database
await pgPool.query('SELECT 1')

const logger = {
error: console.error,
info: console.info,
request: ['1', 'true'].includes(REQUEST_LOGGING) ? console.info : () => {}
}

const handler = createHandler({
pgPool,
logger
})
const server = http.createServer(handler)
console.log('Starting the http server on host %j port %s', HOST, PORT)
server.listen(PORT, HOST)
await once(server, 'listening')
console.log(`http://${HOST}:${PORT}`)
1 change: 1 addition & 0 deletions fly.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# fly.toml file generated for spark on 2023-05-16T19:09:01+02:00

app = "spark-stats"
primary_region = "cdg"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
Expand Down
3 changes: 3 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const {
DATABASE_URL = 'postgres://localhost:5432/spark_stats'
} = process.env
56 changes: 56 additions & 0 deletions lib/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Sentry from '@sentry/node'

/**
*
* @param {object} args
* @param {import('pg').Pool} args.pgPool
* @param {import('./typings').Logger} args.logger
* @returns
*/
export const createHandler = ({
pgPool,
logger
}) => {
return (req, res) => {
const start = new Date()
logger.request(`${req.method} ${req.url} ...`)
handler(req, res, pgPool)
.catch(err => errorHandler(res, err, logger))
.then(() => {
logger.request(`${req.method} ${req.url} ${res.statusCode} (${new Date() - start}ms)`)
})
}
}

/**
* @param {import('node:http').IncomingMessage} req
* @param {import('node:http').ServerResponse} res
* @param {import('pg').Pool} pgPool
*/
const handler = async (req, res, pgPool) => {
// TBD
notFound(res)
}

const errorHandler = (res, err, logger) => {
if (err instanceof SyntaxError) {
res.statusCode = 400
res.end('Invalid JSON Body')
} else if (err.statusCode) {
res.statusCode = err.statusCode
res.end(err.message)
} else {
logger.error(err)
res.statusCode = 500
res.end('Internal Server Error')
}

if (res.statusCode >= 500) {
Sentry.captureException(err)
}
}

const notFound = (res) => {
res.statusCode = 404
res.end('Not Found')
}
5 changes: 5 additions & 0 deletions lib/typings.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Logger {
info: typeof console.info;
error: typeof console.error;
request: typeof console.info;
}
59 changes: 59 additions & 0 deletions test/handler.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import http from 'node:http'
import { once } from 'node:events'
import { AssertionError } from 'node:assert'
import pg from 'pg'
import createDebug from 'debug'

import { createHandler } from '../lib/handler.js'
import { DATABASE_URL } from '../lib/config.js'

const debug = createDebug('test')

describe('HTTP request handler', () => {
/** @type {pg.Pool} */
let pgPool
/** @type {http.Server} */
let server
/** @type {string} */
let baseUrl

before(async () => {
pgPool = new pg.Pool({ connectionString: DATABASE_URL })

const handler = createHandler({
pgPool,
logger: {
info: debug,
error: console.error,
request: debug
}
})

// server = http.createServer((req, res) => { console.log(req.method, req.url); res.end('hello') })
server = http.createServer(handler)
server.listen()
await once(server, 'listening')
baseUrl = `http://127.0.0.1:${server.address().port}`
})

after(async () => {
server.closeAllConnections()
server.close()
await pgPool.end()
})

it('returns 404 for unknown routes', async () => {
const res = await fetch(new URL('/unknown-path', baseUrl))
assertResponseStatus(res, 404)
})
})

const assertResponseStatus = async (res, status) => {
if (res.status !== status) {
throw new AssertionError({
actual: res.status,
expected: status,
message: await res.text()
})
}
}
3 changes: 0 additions & 3 deletions test/smoke.test.js

This file was deleted.

0 comments on commit c1f19b3

Please sign in to comment.