Skip to content
This repository has been archived by the owner on Aug 6, 2024. It is now read-only.

Commit

Permalink
feat: use D1 instead of KV
Browse files Browse the repository at this point in the history
  • Loading branch information
Tsuk1ko committed Jul 7, 2024
1 parent 4ce9024 commit fe28062
Show file tree
Hide file tree
Showing 14 changed files with 113 additions and 78 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
TF_VAR_CLOUDFLARE_ACCOUNT_ID: ${{ steps.fetch_account_id.outputs.account_id }}

- name: Initialize Database
run: |
cd worker
npx wrangler d1 execute uptimeflare-db -y --remote --file=setup.sql
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ steps.fetch_account_id.outputs.account_id }}

# Still need to upload worker to keep it up-to-date (Terraform will fail after first-time setup)
- name: Upload worker
run: |
Expand Down
32 changes: 16 additions & 16 deletions .terraform.lock.hcl

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

16 changes: 8 additions & 8 deletions deploy.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ variable "CLOUDFLARE_ACCOUNT_ID" {
type = string
}

resource "cloudflare_workers_kv_namespace" "uptimeflare_kv" {
resource "cloudflare_d1_database" "uptimeflare_db" {
account_id = var.CLOUDFLARE_ACCOUNT_ID
title = "uptimeflare_kv"
name = "uptimeflare-db"
}

resource "cloudflare_worker_script" "uptimeflare" {
Expand All @@ -28,17 +28,17 @@ resource "cloudflare_worker_script" "uptimeflare" {
module = true
compatibility_date = "2023-11-08"

kv_namespace_binding {
name = "UPTIMEFLARE_STATE"
namespace_id = cloudflare_workers_kv_namespace.uptimeflare_kv.id
d1_database_binding {
name = "UPTIMEFLARE_DB"
database_id = cloudflare_d1_database.uptimeflare_db.id
}
}

resource "cloudflare_worker_cron_trigger" "uptimeflare_worker_cron" {
account_id = var.CLOUDFLARE_ACCOUNT_ID
script_name = cloudflare_worker_script.uptimeflare.name
schedules = [
"* * * * *", # every 1 minute, you can reduce the KV write by increase the worker settings of `kvWriteCooldownMinutes`
"* * * * *", # every 1 minute
]
}

Expand All @@ -49,8 +49,8 @@ resource "cloudflare_pages_project" "uptimeflare" {

deployment_configs {
production {
kv_namespaces = {
UPTIMEFLARE_STATE = cloudflare_workers_kv_namespace.uptimeflare_kv.id
d1_databases = {
UPTIMEFLARE_DB = cloudflare_d1_database.uptimeflare_db.id
}
compatibility_date = "2023-11-08"
compatibility_flags = ["nodejs_compat"]
Expand Down
11 changes: 3 additions & 8 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,8 @@ const nextConfig = {
module.exports = nextConfig

if (process.env.NODE_ENV === 'development') {
const { setupDevBindings } = require('@cloudflare/next-on-pages/next-dev')
setupDevBindings({
bindings: {
UPTIMEFLARE_STATE: {
type: 'kv',
id: 'UPTIMEFLARE_STATE'
}
}
const { setupDevPlatform } = require('@cloudflare/next-on-pages/next-dev')
setupDevPlatform({
configPath: './worker/wrangler-dev.toml',
})
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev",
"preview": "npx @cloudflare/next-on-pages && wrangler pages dev .vercel/output/static --compatibility-flag nodejs_compat --kv UPTIMEFLARE_STATE",
"preview": "npx @cloudflare/next-on-pages && wrangler pages dev .vercel/output/static --compatibility-flag nodejs_compat --d1 UPTIMEFLARE_DB",
"build": "next build",
"build:ci": "next-on-pages",
"start": "next start",
Expand Down
11 changes: 6 additions & 5 deletions pages/api/data.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { workerConfig } from '@/uptime.config'
import { MonitorState } from '@/uptime.types'
import { UptimeFlareStateDb } from '@/util/state'
import { NextRequest } from 'next/server'

export const runtime = 'edge'

export default async function handler(req: NextRequest): Promise<Response> {
const { UPTIMEFLARE_STATE } = process.env as unknown as {
UPTIMEFLARE_STATE: KVNamespace
const { UPTIMEFLARE_DB } = process.env as unknown as {
UPTIMEFLARE_DB: D1Database
}

const stateStr = await UPTIMEFLARE_STATE?.get('state')
if (!stateStr) {
const stateDb = new UptimeFlareStateDb<MonitorState>(UPTIMEFLARE_DB)
const state = await stateDb.get()
if (!state) {
return new Response(JSON.stringify({ error: 'No data available' }), {
status: 500,
headers: {
'Content-Type': 'application/json',
},
})
}
const state = JSON.parse(stateStr) as unknown as MonitorState

let monitors: any = {}

Expand Down
22 changes: 9 additions & 13 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,26 @@ import Head from 'next/head'

import { Inter } from 'next/font/google'
import { MonitorState, MonitorTarget } from '@/uptime.types'
import { KVNamespace } from '@cloudflare/workers-types'
import { pageConfig, workerConfig } from '@/uptime.config'
import OverallStatus from '@/components/OverallStatus'
import Header from '@/components/Header'
import MonitorList from '@/components/MonitorList'
import { Center, Divider, Text } from '@mantine/core'
import MonitorDetail from '@/components/MonitorDetail'
import { UptimeFlareStateDb } from '@/util/state'

export const runtime = 'experimental-edge'
const inter = Inter({ subsets: ['latin'] })

export default function Home({
state: stateStr,
state,
monitors,
}: {
state: string
state: MonitorState | null
monitors: MonitorTarget[]
tooltip?: string
statusPageLink?: string
}) {
let state
if (stateStr !== undefined) {
state = JSON.parse(stateStr) as MonitorState
}

// Specify monitorId in URL hash to view a specific monitor (can be used in iframe)
const monitorId = window.location.hash.substring(1)
if (monitorId) {
Expand All @@ -52,10 +47,10 @@ export default function Home({
<main className={inter.className}>
<Header />

{state === undefined ? (
{!state ? (
<Center>
<Text fw={700}>
Monitor State is not defined now, please check your worker&apos;s status and KV
Monitor State is not defined now, please check your worker&apos;s status and D1
binding!
</Text>
</Center>
Expand Down Expand Up @@ -94,12 +89,13 @@ export default function Home({
}

export async function getServerSideProps() {
const { UPTIMEFLARE_STATE } = process.env as unknown as {
UPTIMEFLARE_STATE: KVNamespace
const { UPTIMEFLARE_DB } = process.env as unknown as {
UPTIMEFLARE_DB: D1Database
}

// Read state as string from KV, to avoid hitting server-side cpu time limit
const state = (await UPTIMEFLARE_STATE?.get('state')) as unknown as MonitorState
const stateDb = new UptimeFlareStateDb<MonitorState>(UPTIMEFLARE_DB)
const state = await stateDb.get()

// Only present these values to client
const monitors = workerConfig.monitors.map((monitor) => {
Expand Down
2 changes: 0 additions & 2 deletions uptime.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ const pageConfig = {
}

const workerConfig = {
// Write KV at most every 3 minutes unless the status changed.
kvWriteCooldownMinutes: 3,
// Define all your monitors here
monitors: [
// Example HTTP Monitor
Expand Down
1 change: 1 addition & 0 deletions uptime.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
type MonitorState = {
version: number
lastUpdate: number
overallUp: number
overallDown: number
Expand Down
38 changes: 38 additions & 0 deletions util/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export class UptimeFlareStateDb<T> {
private stateExist = false

constructor(private db: D1Database) {}

async get(): Promise<T | null> {
const result = await this.db
.prepare("SELECT * FROM kv WHERE key = 'state'")
.first<{ key: 'state'; value: string }>()

this.stateExist = !!result

return result ? this.safeJsonParse(result.value) : null
}

async set(value: T) {
if (this.stateExist) {
await this.db
.prepare("UPDATE kv SET value = ? WHERE key = 'state'")
.bind(JSON.stringify(value))
.run()
} else {
await this.db
.prepare("INSERT INTO kv (key, value) VALUES ('state', ?)")
.bind(JSON.stringify(value))
.run()
this.stateExist = true
}
}

private safeJsonParse(str: string) {
try {
return JSON.parse(str)
} catch {
return null
}
}
}
1 change: 1 addition & 0 deletions worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"vercel-build": "next build",
"deploy": "wrangler deploy",
"dev": "wrangler dev --config wrangler-dev.toml --test-scheduled --persist-to ../.wrangler/state",
"db:init": "wrangler d1 execute uptimeflare-db --config wrangler-dev.toml --local --file=setup.sql --persist-to ../.wrangler/state",
"start": "wrangler dev"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions worker/setup.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- DROP TABLE IF EXISTS kv;
CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT);
39 changes: 15 additions & 24 deletions worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { workerConfig } from '../../uptime.config'
import { formatStatusChangeNotification, getWorkerLocation, notifyWithApprise } from './util'
import { MonitorState } from '../../uptime.types'
import { getStatus } from './monitor'
import { UptimeFlareStateDb } from '../../util/state'

export interface Env {
UPTIMEFLARE_STATE: KVNamespace
UPTIMEFLARE_DB: D1Database
}

export default {
Expand Down Expand Up @@ -73,18 +74,15 @@ export default {
}

// Read state, set init state if it doesn't exist
let state =
((await env.UPTIMEFLARE_STATE.get('state', {
type: 'json',
})) as unknown as MonitorState) ||
({
version: 1,
lastUpdate: 0,
overallUp: 0,
overallDown: 0,
incident: {},
latency: {},
} as MonitorState)
const db = new UptimeFlareStateDb<MonitorState>(env.UPTIMEFLARE_DB)
let state: MonitorState = (await db.get()) || {
version: 1,
lastUpdate: 0,
overallUp: 0,
overallDown: 0,
incident: {},
latency: {},
}
state.overallDown = 0
state.overallUp = 0

Expand Down Expand Up @@ -316,17 +314,10 @@ export default {
console.log(
`statusChanged: ${statusChanged}, lastUpdate: ${state.lastUpdate}, currentTime: ${currentTimeSecond}`
)

// Update state
// Allow for a cooldown period before writing to KV
if (
statusChanged ||
currentTimeSecond - state.lastUpdate >= workerConfig.kvWriteCooldownMinutes * 60 - 10 // Allow for 10 seconds of clock drift
) {
console.log('Updating state...')
state.lastUpdate = currentTimeSecond
await env.UPTIMEFLARE_STATE.put('state', JSON.stringify(state))
} else {
console.log('Skipping state update due to cooldown period.')
}
console.log('Updating state...')
state.lastUpdate = currentTimeSecond
await db.set(state)
},
}
6 changes: 5 additions & 1 deletion worker/wrangler-dev.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
name = "uptimeflare_worker"
main = "src/index.ts"
compatibility_date = "2023-11-08"
kv_namespaces = [{ binding = "UPTIMEFLARE_STATE", id = "UPTIMEFLARE_STATE" }]

[[d1_databases]]
binding = "UPTIMEFLARE_DB"
database_name = "uptimeflare-db"
database_id = "uptimeflare-db-dev"

0 comments on commit fe28062

Please sign in to comment.