|
1 | 1 | // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 |
2 | 2 |
|
3 | 3 | using System.Runtime.InteropServices; |
| 4 | +using System.Threading.Channels; |
4 | 5 |
|
5 | 6 | using Valkey.Glide.Internals; |
6 | 7 | using Valkey.Glide.Pipeline; |
@@ -187,36 +188,41 @@ private void PubSubCallback( |
187 | 188 | IntPtr patternPtr, |
188 | 189 | long patternLen) |
189 | 190 | { |
190 | | - // Work needs to be offloaded from the calling thread, because otherwise we might starve the client's thread pool. |
191 | | - _ = Task.Run(() => |
| 191 | + try |
192 | 192 | { |
193 | | - try |
| 193 | + // Only process actual message notifications, ignore subscription confirmations |
| 194 | + if (!IsMessageNotification((PushKind)pushKind)) |
194 | 195 | { |
195 | | - // Only process actual message notifications, ignore subscription confirmations |
196 | | - if (!IsMessageNotification((PushKind)pushKind)) |
197 | | - { |
198 | | - Logger.Log(Level.Debug, "PubSubCallback", $"PubSub notification received: {(PushKind)pushKind}"); |
199 | | - return; |
200 | | - } |
201 | | - |
202 | | - // Marshal the message from FFI callback parameters |
203 | | - PubSubMessage message = MarshalPubSubMessage( |
204 | | - (PushKind)pushKind, |
205 | | - messagePtr, |
206 | | - messageLen, |
207 | | - channelPtr, |
208 | | - channelLen, |
209 | | - patternPtr, |
210 | | - patternLen); |
211 | | - |
212 | | - // Process the message through the handler |
213 | | - HandlePubSubMessage(message); |
| 196 | + Logger.Log(Level.Debug, "PubSubCallback", $"PubSub notification received: {(PushKind)pushKind}"); |
| 197 | + return; |
214 | 198 | } |
215 | | - catch (Exception ex) |
| 199 | + |
| 200 | + // Marshal the message from FFI callback parameters |
| 201 | + PubSubMessage message = MarshalPubSubMessage( |
| 202 | + (PushKind)pushKind, |
| 203 | + messagePtr, |
| 204 | + messageLen, |
| 205 | + channelPtr, |
| 206 | + channelLen, |
| 207 | + patternPtr, |
| 208 | + patternLen); |
| 209 | + |
| 210 | + // Write to channel (non-blocking with backpressure) |
| 211 | + Channel<PubSubMessage>? channel = _messageChannel; |
| 212 | + if (channel != null) |
216 | 213 | { |
217 | | - Logger.Log(Level.Error, "PubSubCallback", $"Error in PubSub callback: {ex.Message}", ex); |
| 214 | + if (!channel.Writer.TryWrite(message)) |
| 215 | + { |
| 216 | + Logger.Log(Level.Warn, "PubSubCallback", |
| 217 | + $"PubSub message channel full, message dropped for channel {message.Channel}"); |
| 218 | + } |
218 | 219 | } |
219 | | - }); |
| 220 | + } |
| 221 | + catch (Exception ex) |
| 222 | + { |
| 223 | + Logger.Log(Level.Error, "PubSubCallback", |
| 224 | + $"Error in PubSub callback: {ex.Message}", ex); |
| 225 | + } |
220 | 226 | } |
221 | 227 |
|
222 | 228 | private static bool IsMessageNotification(PushKind pushKind) => |
@@ -280,10 +286,58 @@ private void InitializePubSubHandler(BasePubSubSubscriptionConfig? config) |
280 | 286 | return; |
281 | 287 | } |
282 | 288 |
|
283 | | - // Create the PubSub message handler with thread-safe initialization |
284 | 289 | lock (_pubSubLock) |
285 | 290 | { |
| 291 | + // Get performance configuration or use defaults |
| 292 | + PubSubPerformanceConfig perfConfig = config.PerformanceConfig ?? new(); |
| 293 | + |
| 294 | + // Create bounded channel with configurable capacity and backpressure strategy |
| 295 | + BoundedChannelOptions channelOptions = new(perfConfig.ChannelCapacity) |
| 296 | + { |
| 297 | + FullMode = perfConfig.FullMode, |
| 298 | + SingleReader = true, // Optimization: only one processor task |
| 299 | + SingleWriter = false // Multiple FFI callbacks may write |
| 300 | + }; |
| 301 | + |
| 302 | + _messageChannel = Channel.CreateBounded<PubSubMessage>(channelOptions); |
| 303 | + _processingCancellation = new CancellationTokenSource(); |
| 304 | + |
| 305 | + // Create message handler |
286 | 306 | _pubSubHandler = new PubSubMessageHandler(config.Callback, config.Context); |
| 307 | + |
| 308 | + // Start dedicated processing task |
| 309 | + _messageProcessingTask = Task.Run(async () => |
| 310 | + { |
| 311 | + try |
| 312 | + { |
| 313 | + await foreach (PubSubMessage message in _messageChannel.Reader.ReadAllAsync(_processingCancellation.Token)) |
| 314 | + { |
| 315 | + try |
| 316 | + { |
| 317 | + // Thread-safe access to handler |
| 318 | + PubSubMessageHandler? handler = _pubSubHandler; |
| 319 | + if (handler != null && !_processingCancellation.Token.IsCancellationRequested) |
| 320 | + { |
| 321 | + handler.HandleMessage(message); |
| 322 | + } |
| 323 | + } |
| 324 | + catch (Exception ex) |
| 325 | + { |
| 326 | + Logger.Log(Level.Error, "BaseClient", |
| 327 | + $"Error processing PubSub message: {ex.Message}", ex); |
| 328 | + } |
| 329 | + } |
| 330 | + } |
| 331 | + catch (OperationCanceledException) |
| 332 | + { |
| 333 | + Logger.Log(Level.Info, "BaseClient", "PubSub processing cancelled"); |
| 334 | + } |
| 335 | + catch (Exception ex) |
| 336 | + { |
| 337 | + Logger.Log(Level.Error, "BaseClient", |
| 338 | + $"PubSub processing task failed: {ex.Message}", ex); |
| 339 | + } |
| 340 | + }, _processingCancellation.Token); |
287 | 341 | } |
288 | 342 | } |
289 | 343 |
|
@@ -322,41 +376,57 @@ internal virtual void HandlePubSubMessage(PubSubMessage message) |
322 | 376 | private void CleanupPubSubResources() |
323 | 377 | { |
324 | 378 | PubSubMessageHandler? handler = null; |
| 379 | + Channel<PubSubMessage>? channel = null; |
| 380 | + Task? processingTask = null; |
| 381 | + CancellationTokenSource? cancellation = null; |
| 382 | + TimeSpan shutdownTimeout = TimeSpan.FromSeconds(PubSubPerformanceConfig.DefaultShutdownTimeoutSeconds); |
325 | 383 |
|
326 | | - // Acquire lock and capture handler reference, then set to null |
| 384 | + // Acquire lock and capture references, then set to null |
327 | 385 | lock (_pubSubLock) |
328 | 386 | { |
329 | 387 | handler = _pubSubHandler; |
| 388 | + channel = _messageChannel; |
| 389 | + processingTask = _messageProcessingTask; |
| 390 | + cancellation = _processingCancellation; |
| 391 | + |
330 | 392 | _pubSubHandler = null; |
| 393 | + _messageChannel = null; |
| 394 | + _messageProcessingTask = null; |
| 395 | + _processingCancellation = null; |
331 | 396 | } |
332 | 397 |
|
333 | | - // Dispose outside of lock to prevent deadlocks |
334 | | - if (handler != null) |
| 398 | + // Cleanup outside of lock to prevent deadlocks |
| 399 | + try |
335 | 400 | { |
336 | | - try |
337 | | - { |
338 | | - // Create a task to dispose the handler with timeout |
339 | | - var disposeTask = Task.Run(() => handler.Dispose()); |
| 401 | + // Signal shutdown |
| 402 | + cancellation?.Cancel(); |
| 403 | + |
| 404 | + // Complete channel to stop message processing |
| 405 | + channel?.Writer.Complete(); |
340 | 406 |
|
341 | | - // Wait for disposal with timeout (5 seconds) |
342 | | - if (!disposeTask.Wait(TimeSpan.FromSeconds(5))) |
| 407 | + // Wait for processing task to complete (with timeout) |
| 408 | + if (processingTask != null) |
| 409 | + { |
| 410 | + if (!processingTask.Wait(shutdownTimeout)) |
343 | 411 | { |
344 | 412 | Logger.Log(Level.Warn, "BaseClient", |
345 | | - "PubSub handler disposal did not complete within timeout (5 seconds)"); |
| 413 | + $"PubSub processing task did not complete within timeout ({shutdownTimeout.TotalSeconds}s)"); |
346 | 414 | } |
347 | 415 | } |
348 | | - catch (AggregateException ex) |
349 | | - { |
350 | | - // Log the error but continue with disposal |
351 | | - Logger.Log(Level.Warn, "BaseClient", |
352 | | - $"Error cleaning up PubSub resources: {ex.InnerException?.Message ?? ex.Message}", ex); |
353 | | - } |
354 | | - catch (Exception ex) |
355 | | - { |
356 | | - // Log the error but continue with disposal |
357 | | - Logger.Log(Level.Warn, "BaseClient", |
358 | | - $"Error cleaning up PubSub resources: {ex.Message}", ex); |
359 | | - } |
| 416 | + |
| 417 | + // Dispose resources |
| 418 | + handler?.Dispose(); |
| 419 | + cancellation?.Dispose(); |
| 420 | + } |
| 421 | + catch (AggregateException ex) |
| 422 | + { |
| 423 | + Logger.Log(Level.Warn, "BaseClient", |
| 424 | + $"Error during PubSub cleanup: {ex.InnerException?.Message ?? ex.Message}", ex); |
| 425 | + } |
| 426 | + catch (Exception ex) |
| 427 | + { |
| 428 | + Logger.Log(Level.Warn, "BaseClient", |
| 429 | + $"Error during PubSub cleanup: {ex.Message}", ex); |
360 | 430 | } |
361 | 431 | } |
362 | 432 |
|
@@ -403,5 +473,14 @@ private delegate void PubSubAction( |
403 | 473 | /// Lock object for coordinating PubSub handler access and disposal. |
404 | 474 | private readonly object _pubSubLock = new(); |
405 | 475 |
|
| 476 | + /// Channel for bounded message queuing with backpressure support. |
| 477 | + private Channel<PubSubMessage>? _messageChannel; |
| 478 | + |
| 479 | + /// Dedicated background task for processing PubSub messages. |
| 480 | + private Task? _messageProcessingTask; |
| 481 | + |
| 482 | + /// Cancellation token source for graceful shutdown of message processing. |
| 483 | + private CancellationTokenSource? _processingCancellation; |
| 484 | + |
406 | 485 | #endregion private fields |
407 | 486 | } |
0 commit comments