Skip to content

Commit 353cab8

Browse files
committed
feat: enhance document saving logic with fallback mechanism
- Implemented a fallback mechanism for document saving that directly writes to the database if the job queue is unavailable. - Added logging for both successful saves and errors during the fallback process. - Updated Redis retry strategy to improve error handling and connection resilience, including additional target errors for reconnection.
1 parent 04c87ed commit 353cab8

File tree

2 files changed

+56
-15
lines changed

2 files changed

+56
-15
lines changed

packages/hocuspocus.server/src/config/hocuspocus.config.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,45 @@ const configureExtensions = () => {
107107
// Create a new Y.Doc to store the updated state
108108
Y.applyUpdate(ydoc, state)
109109
const newState = Y.encodeStateAsUpdate(ydoc)
110+
const stateBase64 = Buffer.from(newState).toString('base64')
110111

111-
// Add job to queue (firstCreation is determined in worker by checking DB)
112-
await StoreDocumentQueue.add('store-document', {
113-
documentName,
114-
state: Buffer.from(newState).toString('base64'),
115-
context,
116-
commitMessage
117-
})
112+
try {
113+
// Primary: Add job to queue for async processing
114+
await StoreDocumentQueue.add('store-document', {
115+
documentName,
116+
state: stateBase64,
117+
context,
118+
commitMessage
119+
})
120+
} catch (err) {
121+
// Fallback: Direct DB save when queue fails (Redis OOM, connection error)
122+
dbLogger.warn({ err, documentName }, 'Queue unavailable, falling back to direct save')
123+
124+
try {
125+
const existingDoc = await prisma.documents.findFirst({
126+
where: { documentId: documentName },
127+
orderBy: { id: 'desc' },
128+
select: { version: true }
129+
})
130+
131+
await prisma.documents.create({
132+
data: {
133+
documentId: documentName,
134+
commitMessage: commitMessage || '',
135+
version: existingDoc ? existingDoc.version + 1 : 1,
136+
data: Buffer.from(stateBase64, 'base64')
137+
}
138+
})
139+
140+
dbLogger.info({ documentName }, 'Document saved via fallback (direct DB)')
141+
} catch (dbErr) {
142+
dbLogger.error(
143+
{ err: dbErr, documentName },
144+
'Fallback save failed - document may be lost'
145+
)
146+
throw dbErr
147+
}
148+
}
118149
}
119150
})
120151
)

packages/hocuspocus.server/src/lib/redis.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,26 @@ const getRedisConfig = () => {
3939
keepAlive: parseInt(process.env.REDIS_KEEPALIVE || '30000', 10),
4040

4141
// Retry strategy with exponential backoff
42+
// In production: retry forever with capped delay (Redis may restart)
43+
// In development: stop after maxRetries to fail fast
4244
retryStrategy: (times: number) => {
45+
const isProduction = process.env.NODE_ENV === 'production'
4346
const maxRetries = parseInt(process.env.REDIS_MAX_RETRIES || '10', 10)
4447

48+
// Development: fail fast after max retries
49+
if (!isProduction && times > maxRetries) {
50+
redisLogger.error({ times }, 'Redis max retries exceeded (dev mode)')
51+
return null
52+
}
53+
54+
// Production: never give up, but log warnings
4555
if (times > maxRetries) {
46-
redisLogger.error({ times }, 'Redis max retries exceeded')
47-
return null // Stop retrying
56+
redisLogger.warn({ times }, 'Redis reconnecting (exceeded initial retries, will keep trying)')
4857
}
4958

50-
// Exponential backoff: 200ms, 400ms, 800ms, 1600ms, 3200ms, max 5000ms
51-
const delay = Math.min(times * 200, 5000)
59+
// Exponential backoff: 200ms, 400ms, 800ms... capped at 10s in prod, 5s in dev
60+
const maxDelay = isProduction ? 10000 : 5000
61+
const delay = Math.min(times * 200, maxDelay)
5262
redisLogger.warn({ times, delay: `${delay}ms` }, 'Redis reconnecting...')
5363
return delay
5464
},
@@ -67,12 +77,12 @@ const getRedisConfig = () => {
6777
// TLS for production (if needed)
6878
tls: process.env.REDIS_TLS === 'true' ? {} : undefined,
6979

70-
// Reconnect on error
80+
// Reconnect on error - include all connection-related errors
7181
reconnectOnError: (err: Error) => {
72-
const targetErrors = ['READONLY', 'ETIMEDOUT', 'ECONNRESET']
82+
const targetErrors = ['READONLY', 'ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'Connection is closed']
7383
if (targetErrors.some((targetError) => err.message.includes(targetError))) {
74-
redisLogger.warn({ err }, 'Reconnecting on error')
75-
return true // Reconnect
84+
redisLogger.warn({ err: err.message }, 'Reconnecting on error')
85+
return true
7686
}
7787
return false
7888
}

0 commit comments

Comments
 (0)