Implements timeout protection for Excel batch operations with 2-min default, 5-min max, operation-specific overrides, and rich LLM-friendly error messages. Includes 15 passing tests across 3 layers and comprehensive documentation.
14 KiB
Timeout Implementation Guide
Complete guide for using and enhancing the timeout protection system
Overview
Excel batch operations now have timeout protection to prevent indefinite hangs when Excel becomes unresponsive (modal dialogs, data source issues, COM deadlocks).
Key Features:
- Default 2-minute timeout for all operations
- Maximum 5-minute timeout (prevents infinite waits)
- Operation-specific timeout overrides
- Rich error messages with LLM guidance
- Automatic progress logging to stderr (MCP protocol)
Core Implementation
Constants (ExcelBatch.cs)
/// <summary>
/// Default timeout for Excel operations (2 minutes)
/// </summary>
private static readonly TimeSpan DefaultOperationTimeout = TimeSpan.FromMinutes(2);
/// <summary>
/// Maximum allowed timeout (5 minutes) - prevents infinite waits
/// </summary>
private static readonly TimeSpan MaxOperationTimeout = TimeSpan.FromMinutes(5);
IExcelBatch Interface
Task<T> Execute<T>(
Func<ExcelContext, CancellationToken, T> operation,
CancellationToken cancellationToken = default,
TimeSpan? timeout = null); // ← NEW: Optional timeout parameter
Task<T> ExecuteAsync<T>(
Func<ExcelContext, CancellationToken, Task<T>> operation,
CancellationToken cancellationToken = default,
TimeSpan? timeout = null); // ← NEW: Optional timeout parameter
Timeout Enforcement Pattern
// Clamp timeout between default and max
var effectiveTimeout = timeout.HasValue
? TimeSpan.FromMilliseconds(Math.Min(timeout.Value.TotalMilliseconds, MaxOperationTimeout.TotalMilliseconds))
: DefaultOperationTimeout;
// Create linked cancellation token (operation + timeout)
using var timeoutCts = new CancellationTokenSource(effectiveTimeout);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
// Use Task.WaitAsync for timeout enforcement
return await tcs.Task.WaitAsync(linkedCts.Token);
Error Handling Pattern
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
var duration = DateTime.UtcNow - startTime;
var usedMaxTimeout = effectiveTimeout >= MaxOperationTimeout;
Console.Error.WriteLine($"[EXCEL-BATCH] TIMEOUT after {duration.TotalSeconds:F1}s (limit: {effectiveTimeout.TotalMinutes:F1}min, max: {usedMaxTimeout})");
var message = usedMaxTimeout
? $"Excel operation exceeded maximum timeout of {MaxOperationTimeout.TotalMinutes} minutes (actual: {duration.TotalMinutes:F1} min). " +
"This indicates Excel is hung, unresponsive, or the operation is too complex. " +
"Check if Excel is showing a dialog or consider breaking the operation into smaller steps."
: $"Excel operation timed out after {effectiveTimeout.TotalMinutes} minutes (actual: {duration.TotalMinutes:F1} min). " +
$"For large datasets or complex operations, more time may be needed (maximum: {MaxOperationTimeout.TotalMinutes} min).";
throw new TimeoutException(message);
}
Enhanced Result Types
ResultBase Additions
All result types now inherit LLM guidance fields:
public abstract class ResultBase
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public string? FilePath { get; set; }
// ✨ NEW: LLM Guidance Fields
public List<string>? SuggestedNextActions { get; set; }
public Dictionary<string, object>? OperationContext { get; set; }
public bool IsRetryable { get; set; } = true;
public string? RetryGuidance { get; set; }
}
Usage Patterns
Pattern 1: Core Commands (Heavy Operations)
For operations that typically take longer (refresh, data loading), Core commands can pass custom timeout:
// In PowerQueryCommands.cs
public async Task<PowerQueryRefreshResult> RefreshAsync(IExcelBatch batch, string queryName)
{
// Heavy operation: request extended timeout (5 minutes)
return await batch.Execute<PowerQueryRefreshResult>(
(ctx, ct) => {
// Refresh logic...
return result;
},
timeout: TimeSpan.FromMinutes(5) // ← Request 5 min (will be clamped to max)
);
}
Pattern 2: MCP Tool Timeout Exception Handling
MCP tools should catch TimeoutException and enrich with operation-specific guidance:
// In ExcelPowerQueryTool.cs
private static async Task<string> RefreshPowerQueryAsync(...)
{
try
{
var result = await ExcelToolsBase.WithBatchAsync(
batchId,
excelPath,
save: true,
async (batch) => await commands.RefreshAsync(batch, queryName));
return JsonSerializer.Serialize(result, ExcelToolsBase.JsonOptions);
}
catch (TimeoutException ex)
{
// Enrich with operation-specific guidance
var result = new PowerQueryRefreshResult
{
Success = false,
ErrorMessage = ex.Message,
QueryName = queryName,
FilePath = excelPath,
// ✨ Add LLM guidance
SuggestedNextActions = new List<string>
{
"Check if Excel is showing a 'Privacy Level' dialog or other modal dialogs",
"Verify the data source is accessible (network connection, database availability)",
"Consider breaking query into smaller steps if processing large datasets",
"Use batch mode (begin_excel_batch) if not already using it"
},
OperationContext = new Dictionary<string, object>
{
{ "OperationType", "PowerQuery.Refresh" },
{ "QueryName", queryName },
{ "TimeoutReached", true },
{ "UsedMaxTimeout", ex.Message.Contains("maximum timeout") }
},
IsRetryable = !ex.Message.Contains("maximum timeout"), // Don't retry max timeout
RetryGuidance = ex.Message.Contains("maximum timeout")
? "Operation reached maximum timeout - do not retry. Check for Excel dialogs or break into smaller operations."
: "Operation can be retried with longer timeout (up to 5 minutes) if data source is slow."
};
return JsonSerializer.Serialize(result, ExcelToolsBase.JsonOptions);
}
}
Pattern 3: Light Operations (Use Default)
For quick operations (list, get, create), use default 2-minute timeout:
// In PowerQueryCommands.cs
public async Task<PowerQueryListResult> ListAsync(IExcelBatch batch)
{
// Light operation: use default timeout (no parameter needed)
return await batch.Execute<PowerQueryListResult>((ctx, ct) =>
{
// List logic...
return result;
}); // ← Default 2 min timeout
}
Operation-Specific Timeout Recommendations
| Operation Type | Recommended Timeout | Rationale |
|---|---|---|
| List operations | Default (2 min) | Reading metadata is fast |
| Get/View operations | Default (2 min) | Reading single item is fast |
| Create operations | Default (2 min) | Creating objects is typically fast |
| Delete operations | Default (2 min) | Deleting is fast |
| Refresh operations | 5 minutes (max) | Data source queries can be slow |
| Import operations | 5 minutes (max) | Loading large datasets |
| Data Model refresh | 5 minutes (max) | Processing millions of rows |
| VBA execution | Context-dependent | User code complexity varies |
Stderr Logging (MCP Protocol)
Operations automatically log progress to stderr (visible in MCP clients):
[EXCEL-BATCH] Starting operation (timeout: 2.0min)
[EXCEL-BATCH] Completed in 3.5s
Or on timeout:
[EXCEL-BATCH] Starting operation (timeout: 5.0min)
[EXCEL-BATCH] TIMEOUT after 300.2s (limit: 5.0min, max: true)
Integration Checklist
For New Core Commands
- Determine if operation is heavy or light
- Pass
timeout: TimeSpan.FromMinutes(5)for heavy operations - Use default timeout for light operations
- Add timeout context to result on failure
For MCP Tools
- Wrap Core command calls in try-catch for
TimeoutException - Create enriched result with:
SuggestedNextActions(operation-specific steps)OperationContext(timeout details, operation type)IsRetryable = falsefor max timeout casesRetryGuidancewith actionable advice
- Return JSON serialized result (don't re-throw)
For CLI Commands
- Display timeout errors with guidance
- Show suggested next actions in readable format
- Indicate if operation is retryable
Example: Complete Timeout-Aware Implementation
// Core Command (PowerQueryCommands.Refresh.cs)
public async Task<PowerQueryRefreshResult> RefreshAsync(IExcelBatch batch, string queryName)
{
var result = new PowerQueryRefreshResult
{
FilePath = batch.WorkbookPath,
QueryName = queryName,
RefreshTime = DateTime.Now
};
return await batch.Execute<PowerQueryRefreshResult>(
(ctx, ct) =>
{
// Refresh logic (omitted for brevity)
result.Success = true;
return result;
},
timeout: TimeSpan.FromMinutes(5) // ← Request extended timeout
);
}
// MCP Tool (ExcelPowerQueryTool.cs)
private static async Task<string> RefreshPowerQueryAsync(
PowerQueryCommands commands,
string excelPath,
string? queryName,
string? batchId)
{
if (string.IsNullOrEmpty(queryName))
throw new ModelContextProtocol.McpException("queryName is required for refresh action");
try
{
var result = await ExcelToolsBase.WithBatchAsync(
batchId,
excelPath,
save: true,
async (batch) => await commands.RefreshAsync(batch, queryName));
return JsonSerializer.Serialize(result, ExcelToolsBase.JsonOptions);
}
catch (TimeoutException ex)
{
var result = new PowerQueryRefreshResult
{
Success = false,
ErrorMessage = ex.Message,
QueryName = queryName,
FilePath = excelPath,
RefreshTime = DateTime.Now,
SuggestedNextActions = new List<string>
{
"Check if Excel is showing a 'Privacy Level' dialog",
"Verify the data source is accessible",
"Consider using smaller data ranges if processing large datasets",
"Use batch mode (begin_excel_batch) if not already"
},
OperationContext = new Dictionary<string, object>
{
{ "OperationType", "PowerQuery.Refresh" },
{ "QueryName", queryName },
{ "TimeoutReached", true },
{ "UsedMaxTimeout", ex.Message.Contains("maximum timeout") }
},
IsRetryable = !ex.Message.Contains("maximum timeout"),
RetryGuidance = ex.Message.Contains("maximum timeout")
? "Maximum timeout reached - do not retry automatically. Manual intervention needed."
: "Retry with same timeout acceptable if transient issue suspected."
};
return JsonSerializer.Serialize(result, ExcelToolsBase.JsonOptions);
}
}
Testing Strategy
Unit Tests (Fast)
Test timeout clamping logic:
[Fact]
public void Execute_TimeoutClamping_LimitsToMax()
{
var requestedTimeout = TimeSpan.FromMinutes(10); // Request 10 min
var effectiveTimeout = Math.Min(requestedTimeout.TotalMilliseconds, MaxOperationTimeout.TotalMilliseconds);
Assert.Equal(TimeSpan.FromMinutes(5), TimeSpan.FromMilliseconds(effectiveTimeout)); // Clamped to 5 min
}
Integration Tests (Requires Excel)
Test actual timeout behavior:
[Fact]
public async Task Execute_ExceedsTimeout_ThrowsTimeoutException()
{
await using var batch = await ExcelSession.BeginBatchAsync(testFile);
// Operation that takes 3 seconds with 1-second timeout
await Assert.ThrowsAsync<TimeoutException>(async () =>
{
await batch.Execute<int>((ctx, ct) =>
{
Thread.Sleep(3000); // Simulate slow operation
return 0;
}, timeout: TimeSpan.FromSeconds(1));
});
}
Troubleshooting
Symptom: All operations timing out
Likely Cause: Excel showing modal dialog (Privacy Level, Credentials, etc.)
Solution:
- Check if Excel process is visible (should be hidden background process)
- Kill all Excel processes:
taskkill /F /IM excel.exe - Re-run operation
- If persistent, manually open Excel, configure privacy levels, save workbook
Symptom: Max timeout reached frequently
Likely Cause: Operation genuinely too complex or data source very slow
Solution:
- Break operation into smaller chunks (filter data, process in batches)
- Optimize data source (add indexes, reduce query complexity)
- Consider Power Query query folding to push processing to source
Symptom: Timeout too aggressive (legitimate operations failing)
Likely Cause: Default timeout too short for specific scenario
Solution:
- Core Commands: Pass explicit timeout:
timeout: TimeSpan.FromMinutes(5) - Cannot exceed 5-minute max (architectural decision to prevent infinite hangs)
Future Enhancements
Potential Improvements
- Configurable Max Timeout: Allow per-workbook or per-session max timeout override (currently hardcoded 5 min)
- Progress Callbacks: Provide progress updates during long operations
- Timeout Metrics: Collect stats on timeout frequency to identify problematic operations
- Adaptive Timeout: Automatically increase timeout for operations that consistently hit limit
Related Documentation
Last Updated: 2025-01-XX
Author: AI Agent + User Collaboration