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
47 changes: 47 additions & 0 deletions Inngest/Attributes/IdempotencyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace Inngest.Attributes;

/// <summary>
/// Configures idempotency for an Inngest function using a CEL expression.
/// When multiple events result in the same idempotency key, only the first
/// event will be processed; subsequent events with the same key are skipped.
/// </summary>
/// <remarks>
/// Use idempotency to prevent duplicate processing when events may be retried
/// or delivered multiple times, such as webhook handlers or receipt senders.
/// </remarks>
/// <example>
/// <code>
/// // One receipt per contribution - prevents duplicate emails on retry
/// [InngestFunction("send-donor-receipt")]
/// [EventTrigger("contribution/created")]
/// [Idempotency("event.data.contributionId")]
/// public class SendDonorReceiptFunction : IInngestFunction { }
///
/// // Compound key for more specific deduplication
/// [InngestFunction("process-payment")]
/// [EventTrigger("payment/received")]
/// [Idempotency("event.data.paymentId + '-' + event.data.customerId")]
/// public class ProcessPaymentFunction : IInngestFunction { }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class IdempotencyAttribute : Attribute
{
/// <summary>
/// CEL expression that evaluates to the idempotency key.
/// Events with the same key will only be processed once.
/// </summary>
public string Key { get; }

/// <summary>
/// Creates a new idempotency attribute
/// </summary>
/// <param name="key">CEL expression for the idempotency key (e.g., "event.data.orderId")</param>
public IdempotencyAttribute(string key)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException("Idempotency key cannot be null or empty", nameof(key));

Key = key;
}
}
67 changes: 67 additions & 0 deletions Inngest/Attributes/ThrottleAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
namespace Inngest.Attributes;

/// <summary>
/// Configures throttling for an Inngest function.
/// Unlike RateLimit which DROPS events when the limit is exceeded,
/// Throttle QUEUES events and processes them at the configured rate.
/// </summary>
/// <remarks>
/// Use Throttle instead of RateLimit when you cannot afford to lose events,
/// such as payment processing webhooks.
/// </remarks>
/// <example>
/// <code>
/// // Limit to 20 executions per minute, queuing excess events
/// [InngestFunction("payment-processor")]
/// [EventTrigger("payment/received")]
/// [Throttle(20, "1m")]
/// public class PaymentProcessor : IInngestFunction { }
///
/// // Limit to 10 executions per minute per customer, with burst allowance
/// [InngestFunction("customer-processor")]
/// [EventTrigger("customer/action")]
/// [Throttle(10, "1m", Key = "event.data.customerId", Burst = 5)]
/// public class CustomerProcessor : IInngestFunction { }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class ThrottleAttribute : Attribute
{
/// <summary>
/// The maximum number of executions allowed in the period
/// </summary>
public int Limit { get; }

/// <summary>
/// The time period for throttling.
/// Uses Inngest time string format (e.g., "1m", "1h", "1d").
/// </summary>
public string Period { get; }

/// <summary>
/// Optional CEL expression to partition the throttle.
/// When specified, the limit applies per unique value of this expression.
/// </summary>
public string? Key { get; set; }

/// <summary>
/// Maximum number of events to burst through before throttling kicks in.
/// </summary>
public int? Burst { get; set; }

/// <summary>
/// Creates a new throttle attribute
/// </summary>
/// <param name="limit">The maximum number of executions in the period</param>
/// <param name="period">The time period (e.g., "1m", "1h", "1d")</param>
public ThrottleAttribute(int limit, string period)
{
if (limit < 1)
throw new ArgumentException("Throttle limit must be at least 1", nameof(limit));
if (string.IsNullOrWhiteSpace(period))
throw new ArgumentException("Period cannot be null or empty", nameof(period));

Limit = limit;
Period = period;
}
}
72 changes: 72 additions & 0 deletions Inngest/Attributes/TimeoutAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
namespace Inngest.Attributes;

/// <summary>
/// Configures timeouts for an Inngest function to automatically cancel
/// runs that take too long to start or finish.
/// </summary>
/// <remarks>
/// <para>
/// Use timeouts to prevent queue buildup and ensure functions don't hang indefinitely.
/// This is especially important when using concurrency controls, as duplicate webhooks
/// queue up and wait - if a request hangs, the queue grows unbounded.
/// </para>
/// <para>
/// There are two timeout types:
/// <list type="bullet">
/// <item>
/// <term>Start</term>
/// <description>
/// Maximum time a run can wait in the queue before starting.
/// Runs exceeding this timeout are cancelled before executing.
/// </description>
/// </item>
/// <item>
/// <term>Finish</term>
/// <description>
/// Maximum time a run can execute after starting.
/// Runs exceeding this timeout are cancelled during execution.
/// </description>
/// </item>
/// </list>
/// </para>
/// </remarks>
/// <example>
/// <code>
/// // Cancel if function takes longer than 30 seconds to complete
/// [InngestFunction("create-contribution")]
/// [EventTrigger("payment/received")]
/// [Timeout(Finish = "30s")]
/// public class CreateContributionFunction : IInngestFunction { }
///
/// // Cancel if queued longer than 1 minute or runs longer than 2 minutes
/// [InngestFunction("process-order")]
/// [EventTrigger("order/created")]
/// [Timeout(Start = "1m", Finish = "2m")]
/// public class ProcessOrderFunction : IInngestFunction { }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class TimeoutAttribute : Attribute
{
/// <summary>
/// Maximum time a run can wait in the queue before starting.
/// Uses Inngest time string format (e.g., "10s", "1m", "1h").
/// If exceeded, the run is cancelled before it starts.
/// </summary>
public string? Start { get; set; }

/// <summary>
/// Maximum time a run can execute after starting.
/// Uses Inngest time string format (e.g., "30s", "5m", "1h").
/// If exceeded, the run is cancelled during execution.
/// </summary>
public string? Finish { get; set; }

/// <summary>
/// Creates a new timeout attribute.
/// At least one of Start or Finish must be specified.
/// </summary>
public TimeoutAttribute()
{
}
}
25 changes: 25 additions & 0 deletions Inngest/FunctionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ public class FunctionOptions
/// Idempotency key expression (CEL) to prevent duplicate executions
/// </summary>
public string? IdempotencyKey { get; set; }

/// <summary>
/// Timeout configuration to automatically cancel runs that take too long
/// </summary>
public TimeoutOptions? Timeouts { get; set; }
}

/// <summary>
Expand Down Expand Up @@ -292,6 +297,26 @@ public class CancellationOptions
public string? Timeout { get; set; }
}

/// <summary>
/// Timeout configuration to automatically cancel runs that take too long
/// </summary>
public class TimeoutOptions
{
/// <summary>
/// Maximum time a run can wait in the queue before starting.
/// Uses Inngest time string format (e.g., "10s", "1m", "1h").
/// If exceeded, the run is cancelled before it starts.
/// </summary>
public string? Start { get; set; }

/// <summary>
/// Maximum time a run can execute after starting.
/// Uses Inngest time string format (e.g., "30s", "5m", "1h").
/// If exceeded, the run is cancelled during execution.
/// </summary>
public string? Finish { get; set; }
}

/// <summary>
/// Configuration for retry behavior
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion Inngest/Inngest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<PackageReleaseNotes>https://github.com/jmarbutt/InngestDotNet/releases</PackageReleaseNotes>

<!-- Version is set by CI, default for local builds -->
<Version>1.3.7</Version>
<Version>1.4.0</Version>

<!-- Build settings -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down
14 changes: 13 additions & 1 deletion Inngest/InngestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
private readonly string _environment;
private readonly bool _isDev;
private readonly bool _disableCronTriggersInDev;
private readonly string _sdkVersion = "1.3.7";
private readonly string _sdkVersion = "1.4.0";
private readonly string _appId;
private readonly ILogger _logger;
private readonly IInngestFunctionRegistry? _registry;
Expand Down Expand Up @@ -947,6 +947,18 @@
{
functionObj["retries"] = fn.Options.Retry.Attempts.Value;
}

// Timeouts
if (fn.Options.Timeouts != null)
{
var timeouts = new Dictionary<string, object>();
if (fn.Options.Timeouts.Start != null)
timeouts["start"] = fn.Options.Timeouts.Start;
if (fn.Options.Timeouts.Finish != null)
timeouts["finish"] = fn.Options.Timeouts.Finish;
if (timeouts.Count > 0)
functionObj["timeouts"] = timeouts;
}
}

fnArray.Add(functionObj);
Expand Down Expand Up @@ -1463,7 +1475,7 @@
["has_signing_key_fallback"] = !string.IsNullOrEmpty(_signingKeyFallback),
["mode"] = _isDev ? "dev" : "cloud",
["schema_version"] = "2024-05-24",
["authentication_succeeded"] = authenticationSucceeded

Check warning on line 1478 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0.x)

Possible null reference assignment.

Check warning on line 1478 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0.x)

Possible null reference assignment.

Check warning on line 1478 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / check-package

Possible null reference assignment.
};

// Add authenticated fields if signature was verified
Expand Down Expand Up @@ -1504,14 +1516,14 @@
responseObj["event_api_origin"] = _eventApiOrigin;
responseObj["app_id"] = _appId;
responseObj["env"] = _environment;
responseObj["event_key_hash"] = eventKeyHash;

Check warning on line 1519 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0.x)

Possible null reference assignment.

Check warning on line 1519 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0.x)

Possible null reference assignment.

Check warning on line 1519 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / check-package

Possible null reference assignment.
responseObj["framework"] = "aspnetcore";
responseObj["sdk_language"] = "dotnet";
responseObj["sdk_version"] = _sdkVersion;
responseObj["serve_origin"] = serveOrigin;

Check warning on line 1523 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0.x)

Possible null reference assignment.

Check warning on line 1523 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0.x)

Possible null reference assignment.

Check warning on line 1523 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / check-package

Possible null reference assignment.
responseObj["serve_path"] = servePath;

Check warning on line 1524 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0.x)

Possible null reference assignment.

Check warning on line 1524 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0.x)

Possible null reference assignment.

Check warning on line 1524 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / check-package

Possible null reference assignment.
responseObj["signing_key_hash"] = signingKeyHash;

Check warning on line 1525 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0.x)

Possible null reference assignment.

Check warning on line 1525 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0.x)

Possible null reference assignment.

Check warning on line 1525 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / check-package

Possible null reference assignment.
responseObj["signing_key_fallback_hash"] = signingKeyFallbackHash;

Check warning on line 1526 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0.x)

Possible null reference assignment.

Check warning on line 1526 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0.x)

Possible null reference assignment.

Check warning on line 1526 in Inngest/InngestClient.cs

View workflow job for this annotation

GitHub Actions / check-package

Possible null reference assignment.
}

await response.WriteAsJsonAsync(responseObj, _jsonOptions);
Expand Down
34 changes: 34 additions & 0 deletions Inngest/Internal/InngestFunctionRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,40 @@ private static FunctionTrigger[] GetTriggers(Type functionType, Type? eventDataT
hasOptions = true;
}

// Throttle
var throttleAttr = functionType.GetCustomAttribute<ThrottleAttribute>();
if (throttleAttr != null)
{
options.Throttle = new ThrottleOptions
{
Limit = throttleAttr.Limit,
Period = throttleAttr.Period,
Key = throttleAttr.Key,
Burst = throttleAttr.Burst
};
hasOptions = true;
}

// Idempotency
var idempotencyAttr = functionType.GetCustomAttribute<IdempotencyAttribute>();
if (idempotencyAttr != null)
{
options.IdempotencyKey = idempotencyAttr.Key;
hasOptions = true;
}

// Timeout
var timeoutAttr = functionType.GetCustomAttribute<TimeoutAttribute>();
if (timeoutAttr != null && (timeoutAttr.Start != null || timeoutAttr.Finish != null))
{
options.Timeouts = new TimeoutOptions
{
Start = timeoutAttr.Start,
Finish = timeoutAttr.Finish
};
hasOptions = true;
}

return hasOptions ? options : null;
}
}
Loading
Loading