Skip to content

Commit 9eb0660

Browse files
authored
Allow custom timeout configurations (#80)
1 parent 1562197 commit 9eb0660

12 files changed

Lines changed: 288 additions & 104 deletions

File tree

config/terrable_config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config
33
type TerrableConfig struct {
44
Handlers []HandlerMapping
55
GlobalEnvironmentVariables map[string]string
6+
Timeout int
67
}
78

89
type HandlerMapping struct {
@@ -11,4 +12,5 @@ type HandlerMapping struct {
1112
Http map[string]string
1213
Sqs map[string]interface{}
1314
EnvironmentVariables map[string]string
15+
Timeout int
1416
}

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/gorilla/mux v1.8.1
1414
github.com/hashicorp/hcl/v2 v2.21.0
1515
github.com/jedib0t/go-pretty/v6 v6.6.3
16+
github.com/stretchr/testify v1.8.4
1617
github.com/urfave/cli/v2 v2.27.3
1718
github.com/zclconf/go-cty v1.15.0
1819
)
@@ -32,11 +33,13 @@ require (
3233
github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 // indirect
3334
github.com/aws/smithy-go v1.22.1 // indirect
3435
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
36+
github.com/davecgh/go-spew v1.1.1 // indirect
3537
github.com/jmespath/go-jmespath v0.4.0 // indirect
3638
github.com/mattn/go-colorable v0.1.13 // indirect
3739
github.com/mattn/go-isatty v0.0.20 // indirect
3840
github.com/mattn/go-runewidth v0.0.15 // indirect
3941
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
42+
github.com/pmezard/go-difflib v1.0.0 // indirect
4043
github.com/rivo/uniseg v0.2.0 // indirect
4144
github.com/russross/blackfriday/v2 v2.1.0 // indirect
4245
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
@@ -45,4 +48,5 @@ require (
4548
golang.org/x/sys v0.25.0 // indirect
4649
golang.org/x/text v0.17.0 // indirect
4750
golang.org/x/tools v0.24.0 // indirect
51+
gopkg.in/yaml.v3 v3.0.1 // indirect
4852
)

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
9696
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
9797
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
9898
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
99+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
99100
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
100101
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
101102
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

offline/handler_server.go

Lines changed: 85 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package offline
22

33
import (
44
"bufio"
5-
"context"
65
"crypto/md5"
76
"encoding/json"
87
"fmt"
@@ -37,132 +36,104 @@ func ServeHandler(handlerInstance *HandlerInstance, r *mux.Router) {
3736

3837
defer np.Close()
3938

40-
for method, path := range handlerInstance.handlerConfig.Http {
41-
go r.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
42-
handlerExecutionMutex.Lock()
43-
defer handlerExecutionMutex.Unlock()
44-
45-
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
46-
defer cancel()
39+
handleRequestFunc := func(w http.ResponseWriter, r *http.Request, code string) {
40+
handlerExecutionMutex.Lock()
41+
defer handlerExecutionMutex.Unlock()
4742

48-
code := generateHttpHandlerRuntimeCode(handlerInstance, r)
43+
np.Execute(code)
4944

50-
np.Execute(code)
45+
outputChannel := make(chan HandlerOutput, 1)
46+
errorChannel := make(chan error, 1)
5147

52-
outputChannel := make(chan HandlerOutput, 1)
48+
fmt.Printf("%s %s (%s) \n", r.Method, r.URL.Path, handlerInstance.handlerConfig.Name)
49+
start := time.Now()
5350

54-
fmt.Printf("%s %s (%s) \n", r.Method, r.URL.Path, handlerInstance.handlerConfig.Name)
55-
start := time.Now()
51+
go func() {
52+
if err := processOutputStream(np, outputChannel); err != nil {
53+
errorChannel <- err
54+
}
55+
}()
5656

57-
go processOutputStream(np, ctx, outputChannel)
58-
go processErrorStream(np, ctx)
57+
// Start error processing with done channel
58+
go processErrorStream(np)
59+
sendResult(start, w, outputChannel)
60+
}
5961

60-
sendResult(start, ctx, w, outputChannel)
62+
for method, path := range handlerInstance.handlerConfig.Http {
63+
r.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
64+
code := generateHttpHandlerRuntimeCode(handlerInstance, r)
65+
handleRequestFunc(w, r, code)
6166
}).Methods(method)
6267
}
6368

6469
for range handlerInstance.handlerConfig.Sqs {
65-
go r.HandleFunc(fmt.Sprintf("/_sqs/%s", handlerInstance.handlerConfig.Name), func(w http.ResponseWriter, r *http.Request) {
66-
handlerExecutionMutex.Lock()
67-
defer handlerExecutionMutex.Unlock()
68-
69-
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
70-
defer cancel()
71-
70+
r.HandleFunc(fmt.Sprintf("/_sqs/%s", handlerInstance.handlerConfig.Name), func(w http.ResponseWriter, r *http.Request) {
7271
code := generateSqsHandlerRuntimeCode(handlerInstance, r)
73-
74-
np.Execute(code)
75-
76-
outputChannel := make(chan HandlerOutput, 1)
77-
78-
fmt.Printf("%s %s (%s) \n", r.Method, r.URL.Path, handlerInstance.handlerConfig.Name)
79-
start := time.Now()
80-
81-
go processOutputStream(np, ctx, outputChannel)
82-
go processErrorStream(np, ctx)
83-
84-
sendResult(start, ctx, w, outputChannel)
72+
handleRequestFunc(w, r, code)
8573
}).Methods("POST")
8674
}
8775

8876
np.cmd.Wait()
8977
}
9078

91-
func sendResult(startTime time.Time, ctx context.Context, w http.ResponseWriter, outputChannel chan HandlerOutput) {
92-
select {
93-
case parsed := <-outputChannel:
94-
if parsed.err != nil {
95-
fmt.Println(parsed.err)
96-
w.WriteHeader(500)
97-
w.Write([]byte{})
98-
return
99-
}
100-
101-
// Set response headers
102-
for k, header := range parsed.handlerResult.Headers {
103-
w.Header().Set(k, header)
104-
}
105-
106-
// Write status code
107-
w.WriteHeader(int(parsed.handlerResult.StatusCode))
108-
109-
// Write the body
110-
w.Write([]byte(parsed.handlerResult.Body))
111-
fmt.Printf("Completed in %.dms\n\n", time.Since(startTime).Milliseconds())
112-
case <-ctx.Done():
113-
// Handle timeout
114-
w.WriteHeader(http.StatusGatewayTimeout)
79+
func sendResult(startTime time.Time, w http.ResponseWriter, outputChannel chan HandlerOutput) {
80+
parsed := <-outputChannel
81+
if parsed.err != nil {
82+
fmt.Println(parsed.err)
83+
w.WriteHeader(500)
11584
w.Write([]byte{})
116-
117-
fmt.Printf("Request timed out\n")
118-
fmt.Printf("Completed in %.dms\n\n", time.Since(startTime).Milliseconds())
11985
return
12086
}
87+
88+
// Set response headers
89+
for k, header := range parsed.handlerResult.Headers {
90+
w.Header().Set(k, header)
91+
}
92+
93+
// Write status code
94+
w.WriteHeader(int(parsed.handlerResult.StatusCode))
95+
96+
// Write the body
97+
w.Write([]byte(parsed.handlerResult.Body))
98+
fmt.Printf("Completed in %.dms\n\n", time.Since(startTime).Milliseconds())
12199
}
122100

123-
func processOutputStream(np *NodeProcess, ctx context.Context, resultChan chan<- HandlerOutput) {
101+
func processOutputStream(np *NodeProcess, resultChan chan<- HandlerOutput) error {
124102
scanner := bufio.NewReader(np.stdout)
125103

126104
for {
127-
select {
128-
case <-ctx.Done():
129-
return
130-
default:
131-
line, _ := scanner.ReadString('\n')
105+
line, err := scanner.ReadString('\n')
132106

133-
if strings.HasPrefix(line, "TERRABLE_RESULT_START") {
134-
extractedResult, err := extractResult(line)
135-
136-
resultChan <- HandlerOutput{
137-
handlerResult: extractedResult,
138-
err: err,
139-
}
140-
141-
return
107+
if err != nil {
108+
if err == io.EOF {
109+
return nil
142110
}
111+
return err
112+
}
143113

144-
if strings.HasPrefix(line, "CODE_EXECUTION_COMPLETE") {
145-
continue
114+
if strings.HasPrefix(line, "TERRABLE_RESULT_START") {
115+
extractedResult, err := extractResult(line)
116+
resultChan <- HandlerOutput{
117+
handlerResult: extractedResult,
118+
err: err,
146119
}
120+
return nil
121+
}
147122

148-
fmt.Println(line)
123+
if strings.HasPrefix(line, "CODE_EXECUTION_COMPLETE") {
124+
continue
149125
}
126+
127+
fmt.Println(line)
150128
}
151129
}
152130

153-
func processErrorStream(np *NodeProcess, ctx context.Context) {
131+
func processErrorStream(np *NodeProcess) {
154132
scanner := bufio.NewReader(np.stderr)
155133
errorColour := color.New(color.FgHiRed).SprintFunc()
156134

157-
for {
158-
select {
159-
case <-ctx.Done():
160-
return
161-
default:
162-
line, _ := scanner.ReadString('\n')
163-
fmt.Println(errorColour(line))
164-
}
165-
}
135+
line, _ := scanner.ReadString('\n')
136+
fmt.Println(errorColour(line))
166137
}
167138

168139
func generateHttpHandlerRuntimeCode(handler *HandlerInstance, r *http.Request) string {
@@ -222,7 +193,7 @@ func generateHttpHandlerRuntimeCode(handler *HandlerInstance, r *http.Request) s
222193

223194
eventInputJSON, _ := json.Marshal(eventInput)
224195
envVars := generateEnvVars(handler)
225-
return generateJSCode(string(envVars), handler.GetExecutionPath(), string(eventInputJSON))
196+
return generateJSCode(string(envVars), handler.GetExecutionPath(), string(eventInputJSON), handler.handlerConfig.Timeout)
226197
}
227198

228199
func generateEnvVars(handler *HandlerInstance) string {
@@ -274,10 +245,10 @@ func generateSqsHandlerRuntimeCode(handler *HandlerInstance, r *http.Request) st
274245

275246
eventInputJSON, _ := json.Marshal(eventInput)
276247
envVars := generateEnvVars(handler)
277-
return generateJSCode(string(envVars), handler.GetExecutionPath(), string(eventInputJSON))
248+
return generateJSCode(string(envVars), handler.GetExecutionPath(), string(eventInputJSON), handler.handlerConfig.Timeout)
278249
}
279250

280-
func generateJSCode(envVars, executionPath, eventInputJSON string) string {
251+
func generateJSCode(envVars, executionPath, eventInputJSON string, timeoutSeconds int) string {
281252
return fmt.Sprintf(`
282253
const env = %s;
283254
process.env = {};
@@ -290,6 +261,7 @@ func generateJSCode(envVars, executionPath, eventInputJSON string) string {
290261
var transpiledFunction = require('%s');
291262
292263
var eventInput = %s;
264+
const endTime = Date.now() + (%d * 1000);
293265
294266
// Create a fake context object
295267
const context = {
@@ -300,11 +272,22 @@ func generateJSCode(envVars, executionPath, eventInputJSON string) string {
300272
awsRequestId: "local-" + Date.now(),
301273
logGroupName: "local-group",
302274
logStreamName: "local-stream",
303-
getRemainingTimeInMillis: () => 30000,
275+
getRemainingTimeInMillis: () => {
276+
const remaining = endTime - Date.now();
277+
return remaining > 0 ? remaining : 0;
278+
},
304279
callbackWaitsForEmptyEventLoop: true
305280
};
306281
307-
new Promise((resolve, reject) => {
282+
// Create a timeout promise
283+
const timeoutPromise = new Promise((resolve) => {
284+
setTimeout(() => {
285+
resolve({ statusCode: 504 })
286+
}, %d * 1000);
287+
});
288+
289+
// Main execution promise
290+
const executionPromise = new Promise((resolve, reject) => {
308291
const callback = (error, result) => {
309292
if (error) {
310293
reject(error);
@@ -322,19 +305,22 @@ func generateJSCode(envVars, executionPath, eventInputJSON string) string {
322305
} else {
323306
resolve(handlerResult);
324307
}
325-
})
308+
});
309+
310+
// Race between execution and timeout
311+
Promise.race([executionPromise, timeoutPromise])
326312
.then(result => {
327313
console.log("TERRABLE_RESULT_START:" + JSON.stringify({ statusCode: 200, ...result }) + ":TERRABLE_RESULT_END");
328314
})
329315
.catch(error => {
330316
console.error(error);
331317
console.log("TERRABLE_RESULT_START:" + JSON.stringify({
332-
statusCode: 500,
318+
statusCode: error.message.includes('timed out') ? 408 : 500,
333319
headers: {
334320
"Content-Type": "application/json",
335321
},
336322
body: JSON.stringify({
337-
message: "Internal server error",
323+
message: error.message.includes('timed out') ? "Function timed out" : "Internal server error",
338324
errorMessage: error.message,
339325
errorType: error.name,
340326
stackTrace: error.stack
@@ -344,7 +330,7 @@ func generateJSCode(envVars, executionPath, eventInputJSON string) string {
344330
.finally(() => {
345331
complete();
346332
});
347-
`, envVars, executionPath, executionPath, eventInputJSON)
333+
`, envVars, executionPath, executionPath, eventInputJSON, timeoutSeconds, timeoutSeconds)
348334
}
349335

350336
func extractResult(output string) (*handlerResult, error) {

offline/node_handler_wrapper.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ function createContext() {
3838
process: process,
3939
complete: () => {
4040
consoleProxy.log("CODE_EXECUTION_COMPLETE");
41+
buffer = "";
4142
process.stdin.resume();
4243
},
4344
})
44-
}
45+
}

samples/simple/simple-api.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module "simple_api" {
1818
GLOBAL_ENV = "global-env-var"
1919
}
2020

21+
timeout = 3
2122
runtime = "nodejs20.x"
2223

2324
rest_api = {
@@ -60,6 +61,14 @@ module "simple_api" {
6061
}
6162
},
6263

64+
TimeoutDelay: {
65+
timeout = 1
66+
source = "./src/TimeoutDelay.ts"
67+
http = {
68+
GET = "/timeout"
69+
}
70+
},
71+
6372
SqsHandler: {
6473
source = "./src/Sqs.ts"
6574
sqs = {

samples/simple/src/TimeoutDelay.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { DoPromise } from "./Utils";
2+
3+
// Configured in simple-api.tf to time out after 1 second
4+
// This will cause a timeout error, which can be caught by
5+
// the hurl tests to verify the timout logic
6+
7+
const handler = async (event) => {
8+
await DoPromise(2000);
9+
10+
return {
11+
statusCode: 200,
12+
}
13+
}
14+
15+
export { handler };

terrable_build

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version = 0.8.0
1+
version = 0.9.0

0 commit comments

Comments
 (0)