Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const ExportDataSection = (props: Props) => {
<ExportCommunityDataButton
disabled={remainingExports === 0}
pastExports={props.settingsData.archives}
isSuperAdmin={isSuperAdmin}
/>
</SettingsSection>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<Date | null>(null);
const [message, setMessage] = useState<string | null>(null);
Expand Down Expand Up @@ -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) {
Expand All @@ -127,6 +133,32 @@ export const ExportCommunityDataButton = ({ disabled, pastExports }: Props) => {
Processing
</Tag>
);
} else if (hasUrl) {
status = (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 4,
}}
>
<Tag minimal intent={isExpired ? 'warning' : 'success'}>
{isExpired ? 'Expired' : 'Ready'}
</Tag>
{/* Surface the underlying error to super admins so the
intermittent worker failure stays visible, while
still presenting the export as successful. */}
{isSuperAdmin && exportItem.error && (
<Tooltip content={exportItem.error}>
<Icon
icon="info-sign"
intent="warning"
iconSize={12}
/>
</Tooltip>
)}
</span>
);
} else if (exportItem.error) {
status = (
<Tag minimal intent="danger" title={exportItem.error}>
Expand All @@ -152,7 +184,7 @@ export const ExportCommunityDataButton = ({ disabled, pastExports }: Props) => {
<td>{createdAt.toLocaleString()}</td>
<td>{status}</td>
<td>
{!exportItem.isProcessing && !exportItem.error
{hasUrl
? isExpired
? 'Expired'
: expiresAt.toLocaleDateString()
Expand Down
18 changes: 15 additions & 3 deletions server/sequelize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: {
Expand All @@ -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/,
],
},
});

Expand Down
Loading