Skip to content

Commit

Permalink
Merge pull request #4195 from coralproject/develop
Browse files Browse the repository at this point in the history
merge 8.0.0 into main
  • Loading branch information
tessalt authored Mar 27, 2023
2 parents f1c0bba + 753e92d commit 48b1e22
Show file tree
Hide file tree
Showing 60 changed files with 2,681 additions and 151 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/build-test-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,25 +112,25 @@ jobs:
uses: docker/build-push-action@v3
with:
push: true
tags: ${{ env.IMAGE_REPOSITORY }}:${MAJOR_TAG}
tags: ${{ env.IMAGE_REPOSITORY }}:${{ env.MAJOR_TAG }}
build-args: |
REVISION_HASH=${GITHUB_SHA}
REVISION_HASH=${{ env.GITHUB_SHA }}
-
name: Build, Tag, Push Minor Tag
uses: docker/build-push-action@v3
with:
push: true
tags: ${{ env.IMAGE_REPOSITORY }}:${MINOR_TAG}
tags: ${{ env.IMAGE_REPOSITORY }}:${{ env.MINOR_TAG }}
build-args: |
REVISION_HASH=${GITHUB_SHA}
REVISION_HASH=${{ env.GITHUB_SHA }}
-
name: Build, Tag, Push Patch Tag
uses: docker/build-push-action@v3
with:
push: true
tags: ${{ env.IMAGE_REPOSITORY }}:${{ env.PATCH_TAG }}
build-args: |
REVISION_HASH=${GITHUB_SHA}
REVISION_HASH=${{ env.GITHUB_SHA }}
-
name: Deploy Static Assets to GCS Bucket
run: |
Expand Down
3 changes: 3 additions & 0 deletions config/jest.cache.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
projects: ["<rootDir>/jest/server.cache.config.js"],
};
30 changes: 30 additions & 0 deletions config/jest/server.cache.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const path = require("path");

module.exports = {
displayName: "cache",
rootDir: "../../",
roots: ["<rootDir>/src/core/server/data/cache"],
collectCoverageFrom: ["**/*.{js,jsx,mjs,ts,tsx}"],
coveragePathIgnorePatterns: ["/node_modules/"],
setupFilesAfterEnv: ["<rootDir>/src/core/server/test/setupTestFramework.ts"],
testMatch: ["**/*.spec.{js,jsx,mjs,ts,tsx}"],
testPathIgnorePatterns: ["/node_modules/", "/client/"],
testEnvironment: "node",
testURL: "http://localhost",
transform: {
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest",
},
moduleNameMapper: {
"^coral-server/(.*)$": "<rootDir>/src/core/server/$1",
"^coral-common/(.*)$": "<rootDir>/src/core/common/$1",
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
globals: {
"ts-jest": {
babelConfig: true,
tsConfig: path.resolve(__dirname, "../../src/tsconfig.json"),
},
},
preset: "ts-jest",
runInBand: true,
};
6 changes: 5 additions & 1 deletion config/jest/server.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ module.exports = {
coveragePathIgnorePatterns: ["/node_modules/"],
setupFilesAfterEnv: ["<rootDir>/src/core/server/test/setupTestFramework.ts"],
testMatch: ["**/*.spec.{js,jsx,mjs,ts,tsx}"],
testPathIgnorePatterns: ["/node_modules/", "/client/"],
testPathIgnorePatterns: [
"/node_modules/",
"/client/",
"/src/core/server/data/cache/",
],
testEnvironment: "node",
testURL: "http://localhost",
transform: {
Expand Down
15 changes: 15 additions & 0 deletions config/paths.cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import fs from "fs";
import path from "path";
import appPaths from "../src/core/build/paths";

// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebookincubator/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath: string) =>
path.resolve(appDirectory, relativePath);

// config after eject: we're in ./config/
export default {
...appPaths,
appJestConfig: resolveApp("config/jest.cache.config.js"),
};
72 changes: 72 additions & 0 deletions docs/docs/migrating-7-to-8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: Migrating from v7.x to v8+
---

Coral v8.0.0 does not include incompatible API changes, however, it includes a large change to the way data retrieval works, the **Coral Redis Cache**, that may impact resource needs. You may wish to make infrastructure or hosting changes to better optimize for v8+.

## Coral Redis Cache

The redis cache is a means of speeding up the retrieval speed of a comment stream by caching its comments, users, and comment actions data in redis and retrieving it from redis instead of Mongo.

We created the redis cache because retrieving extremely large and highly active comment streams is very expensive to query when using Mongo and GraphQL by itself. An optimization was needed to be able to handle streams with 4000+ comments which has possibly 500+ active users on the stream.

We created the redis cache to be able to handle streams of this size (and larger) while maintaining usable stream performance.

We also created the redis cache to avoid the ever ballooning costs of scaling up our Mongo instance to handle these ever larger comment streams. Now that redis is serving the hot traffic of our newsrooms straight from memory instead of retrieving it from a slower database, we are able to downscale our Mongo instances. This nets us a huge cost savings as Mongo database instances are expensive to scale up.

## Differences between Coral v7.x and v8+

Coral now relies heavily on its redis instance and somewhat more on the compute and memory performance of its individual Coral deployments.

Data for a stream is now primarily retrieved from redis. Mongo now serves as a write store for the comment data. This nets you the best of both worlds, fast data read speeds and reliable long term data persistence.

## Differences in resource demands and requirements

Unfortunately, yes, some changes have to be made. Namely, it is wise to beef up whatever redis instance you are using with Coral. It is also recommended that you beef up the CPU performance of whatever pods, vm's, or containers you have hosting Coral as well as provisioning more RAM to each Coral instance (or pod).

### Redis

With the redis cache enabled, Coral's redis instances have roughly 5 GB of RAM and can handle 500 MB/s of throughput per newsroom tenant.

### CPU and RAM

We quadrupled the CPU and RAM of each of our Coral instances. Coral usually runs in a pod (deployed Docker container) within a node on kubernetes. Typically you see allocations of this being milli-cpus (mCPU's) and MB of RAM.

Example allocation for a single Coral pod:

| No Redis Cache | With Redis Cache |
|-------------------------------|-------------------------------|
| 350 mCPU | 1500 mCPU |
| 1024 MB RAM | 4096 MB RAM |
| 5-10 concurrent users per pod | 250+ concurrent users per pod |

An added benefit to adding more CPU to the pod is that web socket traffic will also be fulfilled more quickly on top of the redis cache improvements. This will result in a snappier stream behaviour for your users.

## Infrastructure costs

It may appear that this may lead to increased hosting costs, but we believe it will be net neutral or cheaper.

The optimizations from the redis cache allow each pod to resolve comment and web socket data a lot more quickly. By upping the CPU and RAM, along with using the redis cache changes, each pod can handle 10-100x more traffic than a previous pod could. This is because the pod is now directly drinking from the proverbial redis fire-hose.

We have to add more CPU because the pod needs it to devour the rapid data we get from redis. We also add RAM because Coral is now capable of fully caching per-request comment data in memory to optimize the GraphQL resolver resolutions.

By adding CPU and RAM along with redis cache, you supercharge your Coral API when serving comment stream traffic.

In most cases, you will see that you now need less pods to handle your traffic and will likely see a cost savings for your hosting.

You're making a trade of 4x more CPU/RAM for 10-100x faster processing of web requests.

Because of the reduced demand on mongo, We were able to scale one of our largest mongo instances down accordingly:

| No Redis Cache | With Redis Cache |
|------------------------|------------------------|
| 192 GB of RAM | 64 GB of RAM |
| 48 vCPUs | 16 vCPUs |
| 96,000 max connections | 32,000 max connections |

This is a 3x reduction in hosting requirements for Mongo when using the redis cache. Since Mongo is the most expensive hosting cost when serving Coral, this should be a dramatic cost saving for you as well.

## Managing Story Caching

Though caching is all handled automatically, you can manage caching on a per-story basis if desired. Find the story via the Stories tab in your Coral Admin, then click the `...` actions for that story. In the Story Details Drawer, you will see a button labelled **Recache Story** if the story has not been cached, and if it has, you will see both **Recache Story** and **Uncache Story**. Clicking **Uncache Story** will manually invalidate the Redis cache.

1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ module.exports = {
},
"migrating-5-to-6",
"migrating-6-to-7",
"migrating-7-to-8",
],
},
{
Expand Down
21 changes: 18 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coralproject/talk",
"version": "7.4.9",
"version": "8.0.0",
"author": "The Coral Project",
"homepage": "https://coralproject.net/",
"sideEffects": [
Expand Down Expand Up @@ -58,6 +58,7 @@
"start:webpackDevServer": "ts-node --transpile-only ./scripts/start.ts",
"start": "NODE_ENV=production node dist/index.js",
"test": "node --trace-warnings scripts/test.js --env=jsdom",
"test:cache": "node --trace-warnings scripts/test.cache.js --env=jsdom",
"tscheck:client": "tsc --project ./src/core/client/tsconfig.json --noEmit",
"tscheck:scripts": "tsc --project ./tsconfig.json --noEmit",
"tscheck:server": "tsc --project ./src/tsconfig.json --noEmit",
Expand Down
39 changes: 39 additions & 0 deletions scripts/test.cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env node

"use strict";
// Set timezone to UTC for stable tests.
process.env.TZ = "UTC";

// Allow importing typescript files.
require("ts-node/register");

// Apply all the configuration provided in the .env file.
require("dotenv").config();

// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = "test";
process.env.NODE_ENV = "test";

// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on("unhandledRejection", (err) => {
throw err;
});

const paths = require("../config/paths.cache.ts").default;

const jest = require("jest");
const argv = process.argv.slice(2);
argv.push("--config", paths.appJestConfig);

// Watch unless on CI or in coverage mode
if (
!process.env.CI && // ensure that the ci env var is not set.
argv.indexOf("--ci") < 0 && // ensure that the ci flag is not passed
argv.indexOf("--coverage") < 0 // ensure that the coverage flag is not passed
) {
argv.push("--watch");
}

jest.run(argv);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Environment, graphql } from "relay-runtime";

import {
createFetch,
fetchQuery,
FetchVariables,
} from "coral-framework/lib/relay";

import { FetchStoryCachedQuery as QueryTypes } from "coral-admin/__generated__/FetchStoryCachedQuery.graphql";

const FetchStoryCached = createFetch(
"fetchStoryCachedQuery",
(environment: Environment, variables: FetchVariables<QueryTypes>) => {
return fetchQuery<QueryTypes>(
environment,
graphql`
query FetchStoryCachedQuery($storyID: ID!) {
story(id: $storyID) {
id
cached
}
}
`,
variables,
{ force: true }
);
}
);

export default FetchStoryCached;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Localized } from "@fluent/react/compat";
import React, { FunctionComponent, useCallback, useState } from "react";

import InvalidateCachedStoryMutation from "coral-admin/components/StoryInfoDrawer/InvalidateCachedStoryMutation";
import { useFetch, useMutation } from "coral-framework/lib/relay";
import { Button, Flex } from "coral-ui/components/v2";

import FetchStoryCached from "./FetchStoryCached";

export interface Props {
storyID: string;
}

const checkCachedPollTimeMS = 2000;

const InvalidateCachedStoryAction: FunctionComponent<Props> = ({ storyID }) => {
const fetchStory = useFetch(FetchStoryCached);
const [triggered, setTriggered] = useState(false);
const invalidateStory = useMutation(InvalidateCachedStoryMutation);

const checkIfCached = useCallback(async () => {
const { story } = await fetchStory({ storyID });
if (!story || story.cached) {
setTimeout(checkIfCached, checkCachedPollTimeMS);
} else if (story && !story.cached) {
setTriggered(false);
}
}, [storyID, fetchStory]);

const onInvalidateCache = useCallback(async () => {
if (triggered) {
return;
}

await invalidateStory({ id: storyID });
setTriggered(true);
}, [storyID, invalidateStory, triggered]);

return (
<Flex>
<Button
disabled={triggered}
type="button"
onClick={onInvalidateCache}
variant="outlined"
color="mono"
>
{triggered ? (
<Localized id="storyInfoDrawer-cacheStory-uncaching">
Uncaching
</Localized>
) : (
<Localized id="storyInfoDrawer-cacheStory-uncacheStory">
Uncache story
</Localized>
)}
</Button>
</Flex>
);
};

export default InvalidateCachedStoryAction;
Loading

0 comments on commit 48b1e22

Please sign in to comment.