mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-08 08:57:22 +08:00
perf: throttle address activity updates (#1104)
* perf: throttle address activity updates * docs: record address activity write throttling * refactor: inline address activity interval * test: cover address activity throttling
This commit is contained in:
@@ -14,6 +14,12 @@
|
|||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
|
- fix: |Worker| 地址活跃时间保活增加 1 天写入窗口,用户设置和邮箱访问不再重复更新近期活跃地址,降低 D1 写入量(issue #1103)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- fix: |E2E| 新增近期地址活跃时间不会被用户设置接口重复写入的回归测试
|
||||||
|
|
||||||
## v1.10.0
|
## v1.10.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -14,6 +14,12 @@
|
|||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
|
- fix: |Worker| Throttle address-activity touches to one write per day so user settings and mailbox access do not repeatedly update recently active addresses, reducing D1 writes (issue #1103)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- fix: |E2E| Add regression coverage ensuring user settings do not rewrite recent address activity timestamps
|
||||||
|
|
||||||
## v1.10.0
|
## v1.10.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import {
|
||||||
|
WORKER_URL,
|
||||||
|
createTestAddress,
|
||||||
|
deleteAddress,
|
||||||
|
hashPassword,
|
||||||
|
} from '../../fixtures/test-helpers';
|
||||||
|
|
||||||
|
const waitForNextTimestamp = () => new Promise((resolve) => setTimeout(resolve, 1_100));
|
||||||
|
|
||||||
|
test.describe('Address activity throttling', () => {
|
||||||
|
test('does not rewrite recently active addresses from user settings', async ({ request }) => {
|
||||||
|
const email = `activity-throttle-${Date.now()}@test.example.com`;
|
||||||
|
const password = hashPassword('test-password-123');
|
||||||
|
const address = await createTestAddress(request, 'activity-throttle');
|
||||||
|
let userId: number | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settingsRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
||||||
|
data: { enable: true, enableMailVerify: false },
|
||||||
|
});
|
||||||
|
expect(settingsRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
|
||||||
|
data: { email, password },
|
||||||
|
});
|
||||||
|
expect(registerRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
|
||||||
|
data: { email, password },
|
||||||
|
});
|
||||||
|
expect(loginRes.ok()).toBe(true);
|
||||||
|
const { jwt: userJwt } = await loginRes.json();
|
||||||
|
const payload = JSON.parse(Buffer.from(userJwt.split('.')[1], 'base64url').toString('utf8'));
|
||||||
|
userId = payload.user_id;
|
||||||
|
|
||||||
|
const bindRes = await request.post(`${WORKER_URL}/user_api/bind_address`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${address.jwt}`,
|
||||||
|
'x-user-token': userJwt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(bindRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const beforeRes = await request.get(`${WORKER_URL}/user_api/bind_address`, {
|
||||||
|
headers: { 'x-user-token': userJwt },
|
||||||
|
});
|
||||||
|
expect(beforeRes.ok()).toBe(true);
|
||||||
|
const before = await beforeRes.json();
|
||||||
|
const initialUpdatedAt = before.results.find(
|
||||||
|
(row: { name: string }) => row.name === address.address,
|
||||||
|
)?.updated_at;
|
||||||
|
expect(initialUpdatedAt).toBeTruthy();
|
||||||
|
|
||||||
|
await waitForNextTimestamp();
|
||||||
|
const userSettingsRes = await request.get(`${WORKER_URL}/user_api/settings`, {
|
||||||
|
headers: { 'x-user-token': userJwt },
|
||||||
|
});
|
||||||
|
expect(userSettingsRes.ok()).toBe(true);
|
||||||
|
await waitForNextTimestamp();
|
||||||
|
|
||||||
|
const afterRes = await request.get(`${WORKER_URL}/user_api/bind_address`, {
|
||||||
|
headers: { 'x-user-token': userJwt },
|
||||||
|
});
|
||||||
|
expect(afterRes.ok()).toBe(true);
|
||||||
|
const after = await afterRes.json();
|
||||||
|
const updatedAt = after.results.find(
|
||||||
|
(row: { name: string }) => row.name === address.address,
|
||||||
|
)?.updated_at;
|
||||||
|
expect(updatedAt).toBe(initialUpdatedAt);
|
||||||
|
} finally {
|
||||||
|
await deleteAddress(request, address.jwt);
|
||||||
|
if (userId) {
|
||||||
|
const deleteUserRes = await request.delete(`${WORKER_URL}/admin/users/${userId}`);
|
||||||
|
expect(deleteUserRes.ok()).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
+26
-2
@@ -250,10 +250,34 @@ export function updateAddressUpdatedAt(
|
|||||||
c.executionCtx.waitUntil((async () => {
|
c.executionCtx.waitUntil((async () => {
|
||||||
try {
|
try {
|
||||||
await c.env.DB.prepare(
|
await c.env.DB.prepare(
|
||||||
`UPDATE address SET updated_at = datetime('now') where name = ?`
|
`UPDATE address SET updated_at = datetime('now')`
|
||||||
|
+ ` WHERE name = ?`
|
||||||
|
+ ` AND (updated_at IS NULL OR updated_at < datetime('now', '-1 day'))`
|
||||||
).bind(address).run();
|
).bind(address).run();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[updateAddressUpdatedAt] failed:", address, e);
|
const errorName = e instanceof Error ? e.name : "UnknownError";
|
||||||
|
console.warn("[updateAddressUpdatedAt] failed:", errorName);
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUserAddressesUpdatedAt(
|
||||||
|
c: Context<HonoCustomType>,
|
||||||
|
userId: number | string | undefined | null
|
||||||
|
): void {
|
||||||
|
if (!userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c.executionCtx.waitUntil((async () => {
|
||||||
|
try {
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`UPDATE address SET updated_at = datetime('now')`
|
||||||
|
+ ` WHERE id IN (SELECT address_id FROM users_address WHERE user_id = ?)`
|
||||||
|
+ ` AND (updated_at IS NULL OR updated_at < datetime('now', '-1 day'))`
|
||||||
|
).bind(userId).run();
|
||||||
|
} catch (e) {
|
||||||
|
const errorName = e instanceof Error ? e.name : "UnknownError";
|
||||||
|
console.warn("[updateUserAddressesUpdatedAt] failed:", errorName);
|
||||||
}
|
}
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import i18n from "../i18n";
|
|||||||
import { UserOauth2Settings, UserSettings } from "../models";
|
import { UserOauth2Settings, UserSettings } from "../models";
|
||||||
import { getJsonSetting, getUserRoles } from "../utils"
|
import { getJsonSetting, getUserRoles } from "../utils"
|
||||||
import { CONSTANTS } from "../constants";
|
import { CONSTANTS } from "../constants";
|
||||||
import { commonGetUserRole } from "../common";
|
import { commonGetUserRole, updateUserAddressesUpdatedAt } from "../common";
|
||||||
import { Jwt } from "hono/utils/jwt";
|
import { Jwt } from "hono/utils/jwt";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -64,17 +64,7 @@ export default {
|
|||||||
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
}, c.env.JWT_SECRET, "HS256");
|
}, c.env.JWT_SECRET, "HS256");
|
||||||
// update address updated_at asynchronously
|
updateUserAddressesUpdatedAt(c, user.user_id);
|
||||||
c.executionCtx.waitUntil((async () => {
|
|
||||||
try {
|
|
||||||
await c.env.DB.prepare(
|
|
||||||
`UPDATE address SET updated_at = datetime('now') where id IN `
|
|
||||||
+ `(SELECT address_id FROM users_address WHERE user_id = ?)`
|
|
||||||
).bind(user.user_id).run();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("[user_api/settings] updateAddressUpdatedAt failed:", user.user_id, e);
|
|
||||||
}
|
|
||||||
})());
|
|
||||||
return c.json({
|
return c.json({
|
||||||
...user,
|
...user,
|
||||||
is_admin: is_admin,
|
is_admin: is_admin,
|
||||||
|
|||||||
Reference in New Issue
Block a user