feat: add Filesystem util and load env util (#7487)
This commit is contained in:
1 parent
164bb988e7
commit
ef6d748784
24 files changed
+389
-194
No files matched your search
@@ -3,14 +3,13 @@ import "regenerator-runtime/runtime"
|
||||
|
||||
import cluster from "cluster"
|
||||
import express from "express"
|
||||
import { GracefulShutdownServer } from "../utils"
|
||||
import { track } from "medusa-telemetry"
|
||||
import { scheduleJob } from "node-schedule"
|
||||
import os from "os"
|
||||
|
||||
import loaders from "../loaders"
|
||||
import Logger from "../loaders/logger"
|
||||
import { isPresent } from "@medusajs/utils"
|
||||
import { isPresent, GracefulShutdownServer } from "@medusajs/utils"
|
||||
|
||||
const EVERY_SIXTH_HOUR = "0 */6 * * *"
|
||||
const CRON_SCHEDULE = EVERY_SIXTH_HOUR
|
||||
|
||||
@@ -2,12 +2,12 @@ import "core-js/stable"
|
||||
import "regenerator-runtime/runtime"
|
||||
|
||||
import express from "express"
|
||||
import { GracefulShutdownServer } from "../utils"
|
||||
import { track } from "medusa-telemetry"
|
||||
import { scheduleJob } from "node-schedule"
|
||||
|
||||
import loaders from "../loaders"
|
||||
import Logger from "../loaders/logger"
|
||||
import { GracefulShutdownServer } from "@medusajs/utils"
|
||||
|
||||
const EVERY_SIXTH_HOUR = "0 */6 * * *"
|
||||
const CRON_SCHEDULE = EVERY_SIXTH_HOUR
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { mkdirSync, rmSync, writeFileSync } from "fs"
|
||||
import { resolve } from "path"
|
||||
import { join } from "path"
|
||||
import { FileSystem } from "@medusajs/utils"
|
||||
|
||||
import loadFeatureFlags from "../feature-flags"
|
||||
|
||||
const distTestTargetDirectorPath = resolve(__dirname, "__ff-test__")
|
||||
|
||||
const getFolderTestTargetDirectoryPath = (folderName: string): string => {
|
||||
return resolve(distTestTargetDirectorPath, folderName)
|
||||
}
|
||||
const filesystem = new FileSystem(join(__dirname, "__ff-test__"))
|
||||
|
||||
const buildFeatureFlag = (
|
||||
key: string,
|
||||
@@ -28,60 +23,42 @@ const buildFeatureFlag = (
|
||||
describe("feature flags", () => {
|
||||
const OLD_ENV = { ...process.env }
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
|
||||
process.env = { ...OLD_ENV }
|
||||
|
||||
rmSync(distTestTargetDirectorPath, { recursive: true, force: true })
|
||||
|
||||
mkdirSync(getFolderTestTargetDirectoryPath("project"), {
|
||||
mode: "777",
|
||||
recursive: true,
|
||||
})
|
||||
|
||||
mkdirSync(getFolderTestTargetDirectoryPath("flags"), {
|
||||
mode: "777",
|
||||
recursive: true,
|
||||
})
|
||||
await filesystem.cleanup()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
process.env = OLD_ENV
|
||||
rmSync(distTestTargetDirectorPath, { recursive: true, force: true })
|
||||
await filesystem.cleanup()
|
||||
})
|
||||
|
||||
it("should load the flag from project", async () => {
|
||||
writeFileSync(
|
||||
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-1.js"),
|
||||
buildFeatureFlag("flag-1", true)
|
||||
)
|
||||
await filesystem.create("flags/flag-1.js", buildFeatureFlag("flag-1", true))
|
||||
|
||||
const flags = await loadFeatureFlags(
|
||||
const flags = loadFeatureFlags(
|
||||
{ featureFlags: { flag_1: false } },
|
||||
undefined,
|
||||
getFolderTestTargetDirectoryPath("flags")
|
||||
join(filesystem.basePath, "flags")
|
||||
)
|
||||
|
||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
||||
})
|
||||
|
||||
it("should load a nested + simple flag from project", async () => {
|
||||
writeFileSync(
|
||||
resolve(getFolderTestTargetDirectoryPath("flags"), "test.js"),
|
||||
buildFeatureFlag("test", false)
|
||||
)
|
||||
|
||||
writeFileSync(
|
||||
resolve(getFolderTestTargetDirectoryPath("flags"), "simpletest.js"),
|
||||
await filesystem.create("flags/test.js", buildFeatureFlag("test", false))
|
||||
await filesystem.create(
|
||||
"flags/simpletest.js",
|
||||
buildFeatureFlag("simpletest", false)
|
||||
)
|
||||
|
||||
const flags = await loadFeatureFlags(
|
||||
const flags = loadFeatureFlags(
|
||||
{ featureFlags: { test: { nested: true }, simpletest: true } },
|
||||
undefined,
|
||||
getFolderTestTargetDirectoryPath("flags")
|
||||
join(filesystem.basePath, "flags")
|
||||
)
|
||||
|
||||
expect(flags.isFeatureEnabled({ test: "nested" })).toEqual(true)
|
||||
@@ -89,15 +66,15 @@ describe("feature flags", () => {
|
||||
})
|
||||
|
||||
it("should load the default feature flags", async () => {
|
||||
writeFileSync(
|
||||
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-1.js"),
|
||||
buildFeatureFlag("flag-1", true)
|
||||
await filesystem.create(
|
||||
"flags/flag-1.js",
|
||||
buildFeatureFlag("flag-1", false)
|
||||
)
|
||||
|
||||
const flags = await loadFeatureFlags(
|
||||
const flags = loadFeatureFlags(
|
||||
{},
|
||||
undefined,
|
||||
getFolderTestTargetDirectoryPath("flags")
|
||||
join(filesystem.basePath, "flags")
|
||||
)
|
||||
|
||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(true)
|
||||
@@ -106,15 +83,14 @@ describe("feature flags", () => {
|
||||
it("should load the flag from env", async () => {
|
||||
process.env.MEDUSA_FF_FLAG_1 = "false"
|
||||
|
||||
writeFileSync(
|
||||
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-1.js"),
|
||||
buildFeatureFlag("flag-1", true)
|
||||
await filesystem.create(
|
||||
"flags/flag-1.js",
|
||||
buildFeatureFlag("flag-1", false)
|
||||
)
|
||||
|
||||
const flags = await loadFeatureFlags(
|
||||
const flags = loadFeatureFlags(
|
||||
{},
|
||||
undefined,
|
||||
getFolderTestTargetDirectoryPath("flags")
|
||||
join(filesystem.basePath, "flags")
|
||||
)
|
||||
|
||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
||||
@@ -122,26 +98,23 @@ describe("feature flags", () => {
|
||||
|
||||
it("should load mix of flags", async () => {
|
||||
process.env.MEDUSA_FF_FLAG_3 = "false"
|
||||
|
||||
writeFileSync(
|
||||
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-1.js"),
|
||||
buildFeatureFlag("flag-1", true)
|
||||
await filesystem.create(
|
||||
"flags/flag-1.js",
|
||||
buildFeatureFlag("flag-1", false)
|
||||
)
|
||||
await filesystem.create(
|
||||
"flags/flag-2.js",
|
||||
buildFeatureFlag("flag-2", false)
|
||||
)
|
||||
await filesystem.create(
|
||||
"flags/flag-3.js",
|
||||
buildFeatureFlag("flag-3", false)
|
||||
)
|
||||
|
||||
writeFileSync(
|
||||
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-2.js"),
|
||||
buildFeatureFlag("flag-2", true)
|
||||
)
|
||||
|
||||
writeFileSync(
|
||||
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-3.js"),
|
||||
buildFeatureFlag("flag-3", true)
|
||||
)
|
||||
|
||||
const flags = await loadFeatureFlags(
|
||||
const flags = loadFeatureFlags(
|
||||
{ featureFlags: { flag_2: false } },
|
||||
undefined,
|
||||
getFolderTestTargetDirectoryPath("flags")
|
||||
join(filesystem.basePath, "flags")
|
||||
)
|
||||
|
||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(true)
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
import { GracefulShutdownServer } from "../graceful-shutdown-server"
|
||||
|
||||
describe("GracefulShutdownServer", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllTimers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
afterEach(() => {})
|
||||
|
||||
it('should add "isShuttingDown" property to the existing server', () => {
|
||||
const server = GracefulShutdownServer.create({ on: jest.fn() } as any)
|
||||
expect(server).toHaveProperty("isShuttingDown")
|
||||
expect(server.isShuttingDown).toEqual(false)
|
||||
})
|
||||
|
||||
it("should listen for client connections and store reference to them", async () => {
|
||||
const onEventMock = jest.fn()
|
||||
|
||||
GracefulShutdownServer.create({ on: onEventMock } as any)
|
||||
|
||||
expect(onEventMock).toBeCalledTimes(3)
|
||||
|
||||
expect(onEventMock.mock.calls[2][0]).toEqual("request")
|
||||
|
||||
const connectEvent: (socket) => any = onEventMock.mock.calls[0][1]
|
||||
|
||||
const onSocketClose = jest.fn()
|
||||
const socket = { on: onSocketClose }
|
||||
connectEvent(socket)
|
||||
expect(socket).toEqual(
|
||||
expect.objectContaining({
|
||||
_idle: true,
|
||||
_connectionId: 1,
|
||||
})
|
||||
)
|
||||
|
||||
const socket2 = { on: onSocketClose }
|
||||
connectEvent(socket2)
|
||||
expect(socket2).toEqual(
|
||||
expect.objectContaining({
|
||||
_idle: true,
|
||||
_connectionId: 2,
|
||||
})
|
||||
)
|
||||
|
||||
const requestMock = onEventMock.mock.calls[2][1]
|
||||
expect(typeof requestMock).toEqual("function")
|
||||
|
||||
const socket3 = { on: onSocketClose }
|
||||
const req = { socket: socket3, on: jest.fn() }
|
||||
const res = { on: jest.fn() }
|
||||
connectEvent(socket3)
|
||||
requestMock(req, res)
|
||||
|
||||
const finishRequestMock = res.on.mock.calls[0][1]
|
||||
|
||||
expect(socket3).toEqual(
|
||||
expect.objectContaining({
|
||||
_idle: false,
|
||||
_connectionId: 3,
|
||||
})
|
||||
)
|
||||
finishRequestMock()
|
||||
expect(socket3).toEqual(
|
||||
expect.objectContaining({
|
||||
_idle: true,
|
||||
_connectionId: 3,
|
||||
})
|
||||
)
|
||||
|
||||
expect(onSocketClose).toBeCalledTimes(3)
|
||||
expect(onSocketClose.mock.calls[0][0]).toEqual("close")
|
||||
})
|
||||
|
||||
it("waits requests to complete before shutting the server down", (done: Function) => {
|
||||
jest.useFakeTimers()
|
||||
|
||||
const onEventMock = jest.fn()
|
||||
const setIntervalSpy = jest.spyOn(global, "setInterval")
|
||||
const setTimeoutSpy = jest.spyOn(global, "setTimeout")
|
||||
const clearIntervalSpy = jest.spyOn(global, "clearInterval")
|
||||
|
||||
const waitTime = 200
|
||||
let closeServerCallback: Function
|
||||
const server = GracefulShutdownServer.create(
|
||||
{
|
||||
close: (callback) => {
|
||||
closeServerCallback = callback
|
||||
},
|
||||
on: onEventMock,
|
||||
} as any,
|
||||
waitTime
|
||||
)
|
||||
|
||||
const requestMock = onEventMock.mock.calls[2][1]
|
||||
const connectEvent: (socket) => any = onEventMock.mock.calls[0][1]
|
||||
|
||||
expect(typeof requestMock).toEqual("function")
|
||||
|
||||
const socket = { on: jest.fn(), destroy: jest.fn() }
|
||||
const req = { socket, on: jest.fn() }
|
||||
const res = { on: jest.fn() }
|
||||
connectEvent(socket)
|
||||
|
||||
requestMock(req, res)
|
||||
|
||||
const finishRequestMock = res.on.mock.calls[0][1]
|
||||
|
||||
server.shutdown().then(() => {
|
||||
done()
|
||||
})
|
||||
|
||||
expect(setTimeoutSpy).toBeCalledTimes(0)
|
||||
expect(setIntervalSpy).toBeCalledTimes(1)
|
||||
expect(setIntervalSpy.mock.calls[0][1]).toEqual(waitTime)
|
||||
expect(clearIntervalSpy).toBeCalledTimes(0)
|
||||
expect(socket.destroy).toBeCalledTimes(0)
|
||||
|
||||
jest.advanceTimersByTime(200)
|
||||
|
||||
expect(socket.destroy).toBeCalledTimes(0)
|
||||
|
||||
finishRequestMock()
|
||||
|
||||
expect(socket.destroy).toBeCalledTimes(0)
|
||||
|
||||
jest.advanceTimersByTime(waitTime)
|
||||
|
||||
expect(socket.destroy).toBeCalledTimes(1)
|
||||
|
||||
closeServerCallback!()
|
||||
})
|
||||
|
||||
it("should force close all connections after the timeout is reached", (done: Function) => {
|
||||
jest.useFakeTimers()
|
||||
|
||||
const onEventMock = jest.fn()
|
||||
const setIntervalSpy = jest.spyOn(global, "setInterval")
|
||||
const setTimeoutSpy = jest.spyOn(global, "setTimeout")
|
||||
const clearIntervalSpy = jest.spyOn(global, "clearInterval")
|
||||
|
||||
const waitTime = 300
|
||||
let closeServerCallback: Function
|
||||
const server = GracefulShutdownServer.create(
|
||||
{
|
||||
close: (callback) => {
|
||||
closeServerCallback = callback
|
||||
},
|
||||
on: onEventMock,
|
||||
} as any,
|
||||
waitTime
|
||||
)
|
||||
|
||||
const requestMock = onEventMock.mock.calls[2][1]
|
||||
const connectEvent: (socket) => any = onEventMock.mock.calls[0][1]
|
||||
|
||||
expect(typeof requestMock).toEqual("function")
|
||||
|
||||
const socket = { on: jest.fn(), destroy: jest.fn() }
|
||||
const req = { socket, on: jest.fn() }
|
||||
const res = { on: jest.fn() }
|
||||
connectEvent(socket)
|
||||
|
||||
requestMock(req, res) // pending request
|
||||
|
||||
const forceTimeout = 600
|
||||
server.shutdown(forceTimeout).then(() => {
|
||||
done()
|
||||
})
|
||||
|
||||
expect(setTimeoutSpy).toBeCalledTimes(1)
|
||||
expect(setTimeoutSpy.mock.calls[0][1]).toEqual(forceTimeout)
|
||||
expect(setIntervalSpy).toBeCalledTimes(1)
|
||||
expect(setIntervalSpy.mock.calls[0][1]).toEqual(waitTime)
|
||||
expect(clearIntervalSpy).toBeCalledTimes(0)
|
||||
expect(socket.destroy).toBeCalledTimes(0)
|
||||
|
||||
jest.advanceTimersByTime(waitTime)
|
||||
expect(socket.destroy).toBeCalledTimes(0)
|
||||
|
||||
jest.advanceTimersByTime(forceTimeout)
|
||||
expect(socket.destroy).toBeCalledTimes(1)
|
||||
|
||||
closeServerCallback!()
|
||||
})
|
||||
})
|
||||
@@ -1,96 +0,0 @@
|
||||
import { Server } from "http"
|
||||
import { Socket } from "net"
|
||||
import Timeout = NodeJS.Timeout
|
||||
|
||||
interface SocketState extends Socket {
|
||||
_idle: boolean
|
||||
_connectionId: number
|
||||
}
|
||||
|
||||
export abstract class GracefulShutdownServer {
|
||||
public isShuttingDown: boolean
|
||||
public abstract shutdown(timeout?: number): Promise<void>
|
||||
public static create<T extends Server>(
|
||||
originalServer: T,
|
||||
waitingResponseTime: number = 200
|
||||
): T & GracefulShutdownServer {
|
||||
let connectionId = 0
|
||||
let shutdownPromise: Promise<void>
|
||||
|
||||
const allSockets: { [id: number]: SocketState } = {}
|
||||
|
||||
const server = originalServer as T & GracefulShutdownServer
|
||||
server.isShuttingDown = false
|
||||
server.shutdown = async (timeout: number = 0): Promise<void> => {
|
||||
if (server.isShuttingDown) {
|
||||
return shutdownPromise
|
||||
}
|
||||
|
||||
server.isShuttingDown = true
|
||||
|
||||
shutdownPromise = new Promise((ok, nok) => {
|
||||
let forceQuit = false
|
||||
let cleanInterval: Timeout
|
||||
|
||||
try {
|
||||
// stop accepting new incoming connections
|
||||
server.close(() => {
|
||||
clearInterval(cleanInterval)
|
||||
ok()
|
||||
})
|
||||
|
||||
if (+timeout > 0) {
|
||||
setTimeout(() => {
|
||||
forceQuit = true
|
||||
}, timeout).unref()
|
||||
}
|
||||
|
||||
cleanInterval = setInterval(() => {
|
||||
if (!Object.keys(allSockets).length) {
|
||||
clearInterval(cleanInterval)
|
||||
}
|
||||
|
||||
for (const key of Object.keys(allSockets)) {
|
||||
const socketId = +key
|
||||
if (forceQuit || allSockets[socketId]._idle) {
|
||||
allSockets[socketId].destroy()
|
||||
delete allSockets[socketId]
|
||||
}
|
||||
}
|
||||
}, waitingResponseTime)
|
||||
} catch (error) {
|
||||
clearInterval(cleanInterval!)
|
||||
return nok(error)
|
||||
}
|
||||
})
|
||||
|
||||
return shutdownPromise
|
||||
}
|
||||
|
||||
const onConnect = (originalSocket) => {
|
||||
connectionId++
|
||||
const socket = originalSocket as SocketState
|
||||
socket._idle = true
|
||||
socket._connectionId = connectionId
|
||||
allSockets[connectionId] = socket
|
||||
|
||||
socket.on("close", () => {
|
||||
delete allSockets[socket._connectionId]
|
||||
})
|
||||
}
|
||||
|
||||
server.on("connection", onConnect)
|
||||
server.on("secureConnection", onConnect)
|
||||
|
||||
server.on("request", (req, res) => {
|
||||
const customSocket = req.socket as SocketState
|
||||
customSocket._idle = false
|
||||
|
||||
res.on("finish", () => {
|
||||
customSocket._idle = true
|
||||
})
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,3 @@ export * from "./exception-formatter"
|
||||
export * from "./middlewares"
|
||||
export * from "./omit-deep"
|
||||
export * from "./remove-undefined-properties"
|
||||
export * from "./graceful-shutdown-server"
|
||||
Reference in new issue
Block a user