Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@ There are a few steps to the setup but it should hopefully be pretty straightfor
4b. (optional) If you want publishing, you'll also need to add a Discord bot token with `wrangler secret put DISCORD_TOKEN`
5. Run `npm run publish` :)

## Data redundancy

This worker is using KV as its main store, but if that was to suffer a problem, you can additionally
save data to R2 or D1.

```toml
[[d1_databases]]
binding = "D1"
database_name = ""
database_id = ""

[[r2_buckets]]
binding = "R2"
bucket_name = ""
```

To setup D1, you need to run the following SQL statement:
`CREATE TABLE IF NOT EXISTS KV (key TEXT UNIQUE, value TEXT)`

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`CREATE TABLE IF NOT EXISTS KV (key TEXT UNIQUE, value TEXT)`
`CREATE TABLE IF NOT EXISTS incidents (key TEXT UNIQUE, value JSON)`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is SQLite, JSON will actually be mapped to NUMERIC.
See datatype3 3.1:

  1. If the declared type contains the string "INT" then it is assigned INTEGER affinity.
  2. If the declared type of the column contains any of the strings "CHAR", "CLOB", or "TEXT" then that column has TEXT affinity. Notice that the type VARCHAR contains the string "CHAR" and is thus assigned TEXT affinity.
  3. If the declared type for a column contains the string "BLOB" or if no type is specified then the column has affinity BLOB.
  4. If the declared type for a column contains any of the strings "REAL", "FLOA", or "DOUB" then the column has REAL affinity.
  5. Otherwise, the affinity is NUMERIC.


## Example
### New Incident
![New Incident](https://user-images.githubusercontent.com/8492901/131903623-352dd6ec-bd7f-470f-9468-4a271c4ddc69.png)
Expand Down
17 changes: 9 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { publishMessage, sendToDiscord } from './discord';
import { attachDebug } from './utils';
import Config from './config';
import { retrieveFromStorage, saveToStorage } from './storage';

export default {
async fetch(req: Request, env: Env) {
Expand Down Expand Up @@ -35,19 +36,19 @@ export default {
const json = await res.json<IncidentResponse>();

await Promise.all(json.incidents.map(async incident => {
const kv = await env.KV.get<Incident>(incident.id, 'json');
const stored = await retrieveFromStorage<Incident>(env, incident.id);

console.log('-----\nIncident ' + incident.id + ' in KV: ' + (kv !== null) + '\n-----');
console.log('-----\nIncident ' + incident.id + ' in storage: ' + (stored !== null) + '\n-----');

if (globalThis.DEBUG?.updateIncident === incident.id) {
// Set update to now so we force an update
incident.updated_at = new Date().toISOString();
}

if (kv === null) {
if (stored === null) {
await this.postNew(incident, env);
} else {
await this.postUpdate(incident, kv, env);
await this.postUpdate(incident, stored, env);
}
}));

Expand All @@ -62,8 +63,8 @@ export default {
if (messageId !== null) {
incident.messageId = messageId;
}
// Update KV
await env.KV.put(incident.id, JSON.stringify(incident));
// Update storage
await saveToStorage(env, incident.id, incident);

// Check if we can publish
if (messageId !== null && Config.PUBLISH_CHANNEL_ID !== '') {
Expand All @@ -89,8 +90,8 @@ export default {
if (incident.updated_at !== cachedIncident.updated_at) {
console.log('Updating incident:', incident.id);

// Update KV
await env.KV.put(incident.id, JSON.stringify(incident));
// Update storage
await saveToStorage(env, incident.id, incident);
// Update Discord
await sendToDiscord(incident, env);
}
Expand Down
50 changes: 50 additions & 0 deletions src/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export async function saveToStorage(env: Env, key: string, value: any) {
const now = new Date().getTime();
const data = JSON.stringify({ ts: now, value });

await Promise.allSettled([
env.KV && env.KV.put(key, data),
env.D1 &&
env.D1.prepare(
`INSERT INTO KV(key, value) VALUES(?1, ?2) ON CONFLICT(key) DO UPDATE SET value = ?2`,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`INSERT INTO KV(key, value) VALUES(?1, ?2) ON CONFLICT(key) DO UPDATE SET value = ?2`,
`INSERT INTO incidents(key, value) VALUES(?1, ?2) ON CONFLICT(key) DO UPDATE SET value = ?2`,

)
.bind(key, data)
.run(),
env.R2 && env.R2.put(key, data),
]);
}

export async function retrieveFromStorage<T>(
env: Env,
key: string,
): Promise<T | null> {
const rawValues = await Promise.allSettled([
env.KV && env.KV.get(key),
env.D1 &&
env.D1.prepare(`SELECT value FROM KV WHERE key = ?`).bind(key).first("value"),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
env.D1.prepare(`SELECT value FROM KV WHERE key = ?`).bind(key).first("value"),
env.D1.prepare(`SELECT value FROM incidents WHERE key = ?`).bind(key).first("value"),

env.R2 && env.R2.get(key).then((object) => object && object.text()),
]);
const values = rawValues.filter(
(x) => x.status === "fulfilled" && x.value,
) as Array<PromiseFulfilledResult<string>>;
// none of our storage providers had the value in stock, so either all of them are
// broken, or its just a new key
if (!values.length) return null;

const parsed: Array<{ ts: number; value: T }> = [];
for (const value of values) {
// lets make sure we handle data corruption cases
try {
parsed.push(JSON.parse(value.value));
} catch {}
}
if (!parsed.length) {
console.error("all data stores hold corrupted data, somehow...", values);
return null;
}

// to account for cases where a write failed, we have the timestamp!
// so we use whatever has the newest timestamp
const freshness = parsed.sort((a, b) => b.ts - a.ts);
return freshness[0].value;
}
4 changes: 3 additions & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ declare module globalThis {

interface Env {
KV: KVNamespace;
D1?: D1Database;
R2?: R2Bucket;
DISCORD_WEBHOOK: string;
DISCORD_TOKEN: string;
}
Expand Down Expand Up @@ -87,4 +89,4 @@ interface DiscordResponse {
interface Debug {
// Allows me to trigger an update for a specific incident
updateIncident?: string;
}
}