Skip to content

Commit

Permalink
Implement debounce configuration
Browse files Browse the repository at this point in the history
Follows similar pattern to recently added `throttle`

The integration test timing could lead to flakes but I tried to leave
a generous buffer to minimize this without making the test too long.

I wanted to add a second integration test for the debounce `timeout`,
but configuring debounce with a `timeout` seems to result in the
function never getting executed on the latest dev server (0.29.6). I
verified this is true with the JS SDK as well.
  • Loading branch information
albertchae committed Sep 3, 2024
1 parent 0fc1ebf commit b1d9daf
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.inngest.springbootdemo.testfunctions;

import com.inngest.FunctionContext;
import com.inngest.InngestFunction;
import com.inngest.InngestFunctionConfigBuilder;
import com.inngest.Step;
import org.jetbrains.annotations.NotNull;

import java.time.Duration;

public class DebouncedFunction extends InngestFunction {

@NotNull
@Override
public InngestFunctionConfigBuilder config(InngestFunctionConfigBuilder builder) {
return builder
.id("DebouncedFunction")
.name("Debounced Function")
.triggerEvent("test/debounced_2_second")
.debounce(Duration.ofSeconds(2));
}

private static int answer = 42;

@Override
public Integer execute(FunctionContext ctx, Step step) {
return step.run("result", () -> answer++, Integer.class);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.inngest.springbootdemo;

import com.inngest.Inngest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.springframework.beans.factory.annotation.Autowired;

import static org.junit.jupiter.api.Assertions.assertEquals;

@IntegrationTest
@Execution(ExecutionMode.CONCURRENT)
class DebouncedFunctionIntegrationTest {
@Autowired
private DevServerComponent devServer;

@Autowired
private Inngest client;

@Test
void testDebouncedFunctionExecutesTrailingEdge() throws Exception {
String firstEvent = InngestFunctionTestHelpers.sendEvent(client, "test/debounced_2_second").getIds()[0];
String secondEvent = InngestFunctionTestHelpers.sendEvent(client, "test/debounced_2_second").getIds()[0];

Thread.sleep(4000);

// With debouncing, the first event is skipped in favor of the second one because they were both sent within
// the debounce period of 2 seconds
assertEquals(0, devServer.runsByEvent(firstEvent).data.length);
RunEntry<Object> secondRun = devServer.runsByEvent(secondEvent).first();

assertEquals("Completed", secondRun.getStatus());
assertEquals(42, secondRun.getOutput());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ protected HashMap<String, InngestFunction> functions() {
addInngestFunction(functions, new InvokeFailureFunction());
addInngestFunction(functions, new TryCatchRunFunction());
addInngestFunction(functions, new ThrottledFunction());
addInngestFunction(functions, new DebouncedFunction());

return functions;
}
Expand Down
2 changes: 2 additions & 0 deletions inngest/src/main/kotlin/com/inngest/Function.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ internal class InternalFunctionConfig
@Json(serializeNull = false)
val throttle: Throttle? = null,
@Json(serializeNull = false)
val debounce: Debounce? = null,
@Json(serializeNull = false)
val batchEvents: BatchEvents? = null,
val steps: Map<String, StepConfig>,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class InngestFunctionConfigBuilder {
private var concurrency: MutableList<Concurrency>? = null
private var retries = 3
private var throttle: Throttle? = null
private var debounce: Debounce? = null
private var batchEvents: BatchEvents? = null

/**
Expand Down Expand Up @@ -147,6 +148,31 @@ class InngestFunctionConfigBuilder {
burst: Int? = null,
): InngestFunctionConfigBuilder = apply { this.throttle = Throttle(limit, period, key, burst) }


/**
* Debounce delays functions for the `period` specified. If an event is sent,
* the function will not run until at least `period` has elapsed.
*
* If any new events are received that match the same debounce `key`, the
* function is rescheduled for another `period` delay, and the triggering
* event is replaced with the latest event received.
*
* See the [Debounce documentation](https://innge.st/debounce) for more
* information.
*
* @param period The period of time to delay after receiving the last trigger to run the function.
* @param key An optional key to use for debouncing.
* @param timeout The maximum time that a debounce can be extended before running.
* If events are continually received within the given period, a function
* will always run after the given timeout period.
*/
@JvmOverloads
fun debounce(
period: Duration,
key: String? = null,
timeout: Duration? = null,
): InngestFunctionConfigBuilder = apply { this.debounce = Debounce(period, key, timeout) }

private fun buildSteps(serveUrl: String): Map<String, StepConfig> {
val scheme = serveUrl.split("://")[0]
return mapOf(
Expand Down Expand Up @@ -179,6 +205,7 @@ class InngestFunctionConfigBuilder {
triggers,
concurrency,
throttle,
debounce,
batchEvents,
steps = buildSteps(serverUrl),
)
Expand Down Expand Up @@ -248,6 +275,20 @@ internal data class Throttle
val burst: Int? = null,
)

internal data class Debounce
@JvmOverloads
constructor(
@KlaxonDuration
val period: Duration,

@Json(serializeNull = false)
val key: String? = null,

@Json(serializeNull = false)
@KlaxonDuration
val timeout: Duration? = null,
)

internal data class BatchEvents
@JvmOverloads
constructor(
Expand Down

0 comments on commit b1d9daf

Please sign in to comment.