chore: Allow websockets and collaboration service to run in the same process (#2674)

This commit is contained in:
Tom Moor 2021-10-19 21:18:20 -07:00 committed by GitHub
parent 3610a7f4a2
commit d443abfc57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 67 additions and 76 deletions

View File

@ -29,8 +29,8 @@ REDIS_URL=redis://localhost:6479
URL=http://localhost:3000 URL=http://localhost:3000
PORT=3000 PORT=3000
# ALPHA See [documentation](docs/SERVICES.md) on running the alpha version of # See [documentation](docs/SERVICES.md) on running a separate collaboration
# the collaboration server. # server, for normal operation this does not need to be set.
COLLABORATION_URL= COLLABORATION_URL=
# To support uploading of images for avatars and document attachments an # To support uploading of images for avatars and document attachments an

View File

@ -96,9 +96,7 @@ function SettingsSidebar() {
label={t("Security")} label={t("Security")}
/> />
)} )}
{can.update && {can.update && env.DEPLOYMENT !== "hosted" && (
env.COLLABORATION_URL &&
env.DEPLOYMENT !== "hosted" && (
<SidebarLink <SidebarLink
to="/settings/features" to="/settings/features"
icon={<BeakerIcon color="currentColor" />} icon={<BeakerIcon color="currentColor" />}

View File

@ -9,7 +9,6 @@ import Checkbox from "components/Checkbox";
import Heading from "components/Heading"; import Heading from "components/Heading";
import HelpText from "components/HelpText"; import HelpText from "components/HelpText";
import Scene from "components/Scene"; import Scene from "components/Scene";
import env from "env";
import useCurrentTeam from "hooks/useCurrentTeam"; import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores"; import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts"; import useToasts from "hooks/useToasts";
@ -53,8 +52,6 @@ function Features() {
all team members. all team members.
</Trans> </Trans>
</HelpText> </HelpText>
{env.COLLABORATION_URL && (
<Checkbox <Checkbox
label={t("Collaborative editing")} label={t("Collaborative editing")}
name="collaborativeEditing" name="collaborativeEditing"
@ -62,7 +59,6 @@ function Features() {
onChange={handleChange} onChange={handleChange}
note="When enabled multiple people can edit documents at the same time (Beta)" note="When enabled multiple people can edit documents at the same time (Beta)"
/> />
)}
</Scene> </Scene>
); );
} }

View File

@ -32,14 +32,13 @@ At least one worker process is required to process the [queues](../server/queues
## Collaboration ## Collaboration
The service is in alpha and as such is not started by default. It must run The service is in alpha and as such is not started by default. Start the service with:
separately to the `websockets` service, and will not start in the same process.
The `COLLABORATION_URL` must be set to the publicly accessible URL when running
the service. For example, if the app is hosted at `https://docs.example.com` you
may use something like: `COLLABORATION_URL=wss://docs-collaboration.example.com`.
Start the service with:
```bash ```bash
yarn start --services=collaboration yarn start --services=collaboration
``` ```
The collaboration service is ideally run on a separate server. If this is the
case the `COLLABORATION_URL` env can be set to the publicly accessible URL. For
example, if the app is hosted at `https://docs.example.com` you may use something
like: `COLLABORATION_URL=wss://docs-collaboration.example.com`.

View File

@ -10,7 +10,7 @@
"build:webpack": "webpack --config webpack.config.prod.js", "build:webpack": "webpack --config webpack.config.prod.js",
"build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server", "build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server",
"start": "node ./build/server/index.js", "start": "node ./build/server/index.js",
"dev": "yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=websockets,admin,web,worker\" \"node build/server/index.js --services=collaboration --port=4000\"", "dev": "yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=collaboration,websockets,admin,web,worker\"",
"dev:watch": "nodemon --exec \"yarn build:server && yarn build:i18n && yarn dev\" -e js --ignore build/ --ignore app/ --ignore flow-typed/", "dev:watch": "nodemon --exec \"yarn build:server && yarn build:i18n && yarn dev\" -e js --ignore build/ --ignore app/ --ignore flow-typed/",
"lint": "eslint app server shared", "lint": "eslint app server shared",
"deploy": "git push heroku master", "deploy": "git push heroku master",
@ -101,7 +101,6 @@
"koa-body": "^4.2.0", "koa-body": "^4.2.0",
"koa-compress": "2.0.0", "koa-compress": "2.0.0",
"koa-convert": "1.2.0", "koa-convert": "1.2.0",
"koa-easy-ws": "^1.3.0",
"koa-helmet": "5.2.0", "koa-helmet": "5.2.0",
"koa-jwt": "^3.6.0", "koa-jwt": "^3.6.0",
"koa-logger": "^3.2.1", "koa-logger": "^3.2.1",
@ -175,6 +174,7 @@
"uuid": "^8.3.2", "uuid": "^8.3.2",
"validator": "5.2.0", "validator": "5.2.0",
"winston": "^3.3.3", "winston": "^3.3.3",
"ws": "^7.5.3",
"y-indexeddb": "^9.0.6", "y-indexeddb": "^9.0.6",
"y-prosemirror": "^1.0.9", "y-prosemirror": "^1.0.9",
"yjs": "^13.5.12" "yjs": "^13.5.12"

View File

@ -33,9 +33,17 @@ const serviceNames = uniq(
// The number of processes to run, defaults to the number of CPU's available // The number of processes to run, defaults to the number of CPU's available
// for the web service, and 1 for collaboration during the beta period. // for the web service, and 1 for collaboration during the beta period.
const processCount = serviceNames.includes("collaboration") let processCount = env.WEB_CONCURRENCY || undefined;
? 1
: env.WEB_CONCURRENCY || undefined; if (serviceNames.includes("collaboration")) {
if (env.WEB_CONCURRENCY !== 1) {
Logger.info(
"lifecycle",
"Note: Restricting process count to 1 due to use of collaborative service"
);
}
processCount = 1;
}
// This function will only be called once in the original process // This function will only be called once in the original process
function master() { function master() {
@ -72,15 +80,6 @@ async function start(id: string, disconnect: () => void) {
router.get("/_health", (ctx) => (ctx.body = "OK")); router.get("/_health", (ctx) => (ctx.body = "OK"));
app.use(router.routes()); app.use(router.routes());
if (
serviceNames.includes("websockets") &&
serviceNames.includes("collaboration")
) {
throw new Error(
"Cannot run websockets and collaboration services in the same process"
);
}
// loop through requested services at startup // loop through requested services at startup
for (const name of serviceNames) { for (const name of serviceNames) {
if (!Object.keys(services).includes(name)) { if (!Object.keys(services).includes(name)) {

View File

@ -6,7 +6,7 @@ export default function present(env: Object): Object {
return { return {
URL: env.URL.replace(/\/$/, ""), URL: env.URL.replace(/\/$/, ""),
CDN_URL: (env.CDN_URL || "").replace(/\/$/, ""), CDN_URL: (env.CDN_URL || "").replace(/\/$/, ""),
COLLABORATION_URL: (env.COLLABORATION_URL || "") COLLABORATION_URL: (env.COLLABORATION_URL || env.URL)
.replace(/\/$/, "") .replace(/\/$/, "")
.replace(/^http/, "ws"), .replace(/^http/, "ws"),
DEPLOYMENT: env.DEPLOYMENT, DEPLOYMENT: env.DEPLOYMENT,

View File

@ -1,5 +1,4 @@
// @flow // @flow
import env from "../env";
import { Team } from "../models"; import { Team } from "../models";
export default function present(team: Team) { export default function present(team: Team) {
@ -8,9 +7,7 @@ export default function present(team: Team) {
name: team.name, name: team.name,
avatarUrl: team.logoUrl, avatarUrl: team.logoUrl,
sharing: team.sharing, sharing: team.sharing,
collaborativeEditing: !!( collaborativeEditing: team.collaborativeEditing,
team.collaborativeEditing && env.COLLABORATION_URL
),
documentEmbeds: team.documentEmbeds, documentEmbeds: team.documentEmbeds,
guestSignin: team.guestSignin, guestSignin: team.guestSignin,
subdomain: team.subdomain, subdomain: team.subdomain,

View File

@ -1,16 +1,17 @@
// @flow // @flow
import http from "http"; import http from "http";
import url from "url";
import { Server } from "@hocuspocus/server"; import { Server } from "@hocuspocus/server";
import Koa from "koa"; import Koa from "koa";
import websocket from "koa-easy-ws"; import WebSocket from "ws";
import Router from "koa-router";
import AuthenticationExtension from "../collaboration/authentication"; import AuthenticationExtension from "../collaboration/authentication";
import LoggerExtension from "../collaboration/logger"; import LoggerExtension from "../collaboration/logger";
import PersistenceExtension from "../collaboration/persistence"; import PersistenceExtension from "../collaboration/persistence";
import TracingExtension from "../collaboration/tracing"; import TracingExtension from "../collaboration/tracing";
export default function init(app: Koa, server: http.Server) { export default function init(app: Koa, server: http.Server) {
const router = new Router(); const path = "/collaboration";
const wss = new WebSocket.Server({ noServer: true });
const hocuspocus = Server.configure({ const hocuspocus = Server.configure({
extensions: [ extensions: [
@ -21,21 +22,15 @@ export default function init(app: Koa, server: http.Server) {
], ],
}); });
// Websockets for collaborative editing server.on("upgrade", function (req, socket, head) {
router.get("/collaboration/:documentName", async (ctx) => { if (req.url.indexOf(path) > -1) {
let { documentName } = ctx.params; const documentName = url.parse(req.url).pathname?.split("/").pop();
if (ctx.ws) { wss.handleUpgrade(req, socket, Buffer.alloc(0), (client) => {
const ws = await ctx.ws(); hocuspocus.handleConnection(client, req, documentName);
hocuspocus.handleConnection(ws, ctx.request, documentName); });
} }
ctx.response.status = 101;
}); });
app.use(websocket());
app.use(router.routes());
app.use(router.allowedMethods());
server.on("shutdown", () => { server.on("shutdown", () => {
hocuspocus.destroy(); hocuspocus.destroy();

View File

@ -16,13 +16,28 @@ import { getUserForJWT } from "../utils/jwt";
const { can } = policy; const { can } = policy;
export default function init(app: Koa, server: http.Server) { export default function init(app: Koa, server: http.Server) {
const path = "/realtime";
// Websockets for events and non-collaborative documents // Websockets for events and non-collaborative documents
const io = IO(server, { const io = IO(server, {
path: "/realtime", path,
serveClient: false, serveClient: false,
cookie: false, cookie: false,
}); });
// Remove the upgrade handler that we just added when registering the IO engine
// And re-add it with a check to only handle the realtime path, this allows
// collaboration websockets to exist in the same process as engine.io.
const listeners = server.listeners("upgrade");
const ioHandleUpgrade = listeners.pop();
server.removeListener("upgrade", ioHandleUpgrade);
server.on("upgrade", function (req, socket, head) {
if (req.url.indexOf(path) > -1) {
ioHandleUpgrade(req, socket, head);
}
});
server.on("shutdown", () => { server.on("shutdown", () => {
Metrics.gaugePerInstance("websockets.count", 0); Metrics.gaugePerInstance("websockets.count", 0);
}); });

View File

@ -9261,14 +9261,6 @@ koa-convert@1.2.0, koa-convert@^1.2.0:
co "^4.6.0" co "^4.6.0"
koa-compose "^3.0.0" koa-compose "^3.0.0"
koa-easy-ws@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/koa-easy-ws/-/koa-easy-ws-1.3.0.tgz#dbf8eeb126ca11eed4c4e3af7904a47c25be2347"
integrity sha512-06lHAwm25HBdplTpwHiZqKcl39vS4KiVzRPneUExCHVvQ9TPHmFlUFO/gjQrUdLj3+kvqQpqdlIPNb91wn6EwA==
dependencies:
debug "^4.1.1"
ws "^7.3.1"
koa-helmet@5.2.0: koa-helmet@5.2.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/koa-helmet/-/koa-helmet-5.2.0.tgz#6529f64dd4539261a9bb0a56e201e4976f0200f0" resolved "https://registry.yarnpkg.com/koa-helmet/-/koa-helmet-5.2.0.tgz#6529f64dd4539261a9bb0a56e201e4976f0200f0"
@ -15496,10 +15488,10 @@ write@1.0.3:
dependencies: dependencies:
mkdirp "^0.5.1" mkdirp "^0.5.1"
ws@^7.2.3, ws@^7.3.1, ws@^7.4.3: ws@^7.2.3, ws@^7.4.3, ws@^7.5.3:
version "7.5.3" version "7.5.5"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
ws@~7.4.2: ws@~7.4.2:
version "7.4.6" version "7.4.6"