diff --git a/client/containers/DashboardSettings/CommunitySettings/CommunityAdminSettings.tsx b/client/containers/DashboardSettings/CommunitySettings/CommunityAdminSettings.tsx index 47cb7f8f2..bbc91a40c 100644 --- a/client/containers/DashboardSettings/CommunitySettings/CommunityAdminSettings.tsx +++ b/client/containers/DashboardSettings/CommunitySettings/CommunityAdminSettings.tsx @@ -60,6 +60,7 @@ const ExportDataSection = (props: Props) => { ); diff --git a/client/containers/DashboardSettings/CommunitySettings/ExportCommunityDataButton.tsx b/client/containers/DashboardSettings/CommunitySettings/ExportCommunityDataButton.tsx index ca49c59b3..a5a897bb9 100644 --- a/client/containers/DashboardSettings/CommunitySettings/ExportCommunityDataButton.tsx +++ b/client/containers/DashboardSettings/CommunitySettings/ExportCommunityDataButton.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react'; -import { Button, Callout, HTMLTable, Spinner, Tag } from '@blueprintjs/core'; +import { Button, Callout, HTMLTable, Icon, Spinner, Tag, Tooltip } from '@blueprintjs/core'; import { apiFetch } from 'client/utils/apiFetch'; @@ -17,9 +17,10 @@ type PastExport = { type Props = { disabled?: boolean; pastExports?: PastExport[]; + isSuperAdmin?: boolean; }; -export const ExportCommunityDataButton = ({ disabled, pastExports }: Props) => { +export const ExportCommunityDataButton = ({ disabled, pastExports, isSuperAdmin }: Props) => { const [isRequesting, setIsRequesting] = useState(false); const [requestedAt, setRequestedAt] = useState(null); const [message, setMessage] = useState(null); @@ -115,10 +116,15 @@ export const ExportCommunityDataButton = ({ disabled, pastExports }: Props) => { const createdAt = new Date(exportItem.createdAt); const expiresAt = new Date(createdAt.getTime() + SEVEN_DAYS_MS); const isExpired = Date.now() > expiresAt.getTime(); + // The worker sometimes fails to write the final result back to + // the database (intermittent connection issue) even though the + // export itself succeeded and produced a download URL. When a URL + // is present we treat the export as successful, regardless of any + // error recorded alongside it. const hasUrl = !exportItem.isProcessing && - !exportItem.error && - typeof exportItem.output === 'string'; + typeof exportItem.output === 'string' && + exportItem.output.length > 0; let status: React.ReactNode; if (exportItem.isProcessing) { @@ -127,6 +133,32 @@ export const ExportCommunityDataButton = ({ disabled, pastExports }: Props) => { Processing ); + } else if (hasUrl) { + status = ( + + + {isExpired ? 'Expired' : 'Ready'} + + {/* Surface the underlying error to super admins so the + intermittent worker failure stays visible, while + still presenting the export as successful. */} + {isSuperAdmin && exportItem.error && ( + + + + )} + + ); } else if (exportItem.error) { status = ( @@ -152,7 +184,7 @@ export const ExportCommunityDataButton = ({ disabled, pastExports }: Props) => { {createdAt.toLocaleString()} {status} - {!exportItem.isProcessing && !exportItem.error + {hasUrl ? isExpired ? 'Expired' : expiresAt.toLocaleDateString() diff --git a/server/sequelize.ts b/server/sequelize.ts index f9d6767ef..ff43e70cc 100644 --- a/server/sequelize.ts +++ b/server/sequelize.ts @@ -69,7 +69,9 @@ export const poolOptions = { min: 2, idle: env.SEQUELIZE_IDLE_TIMEOUT ?? 60_000, acquire: env.SEQUELIZE_ACQUIRE_TIMEOUT ?? 10_000, - maxUses: env.SEQUELIZE_MAX_USES ?? Infinity, + // Recycle connections after a finite number of uses rather than reusing them + // forever, so a long-lived socket that has gone stale is retired gracefully. + maxUses: env.SEQUELIZE_MAX_USES ?? 7500, } satisfies PoolOptions; // this is to avoid thundering herd @@ -87,7 +89,11 @@ export const sequelize = new SequelizeWithId(database_url, { // logQueryParameters: true, logging: false, - dialectOptions: { ssl: useSSL ? { rejectUnauthorized: false } : false }, + dialectOptions: { + ssl: useSSL ? { rejectUnauthorized: false } : false, + // helps maybe fix some connection issues w worker + keepAlive: true, + }, dialect: 'postgres', pool: poolOptions, hooks: { @@ -103,7 +109,13 @@ export const sequelize = new SequelizeWithId(database_url, { }, retry: { max: 3, - match: [/Deadlock/i, ConnectionError], + match: [ + /Deadlock/i, + ConnectionError, + /Connection terminated unexpectedly/i, + /Connection terminated/i, + /ECONNRESET/, + ], }, });