Last active
April 5, 2024 16:42
-
-
Save boseabhishek/1f7953a233ac57b11c4d343dc3173cb5 to your computer and use it in GitHub Desktop.
Wait for a task to finish with timeout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// usage: | |
// go run waitfor.go <num> | |
// where, num can be 1 or 2 or 3 or 4. | |
package main | |
import ( | |
"context" | |
"encoding/json" | |
"fmt" | |
"net/http" | |
"os" | |
"time" | |
) | |
// ----- IMPORTANT ----- | |
// Go routines in funcs: | |
// waitForWithTimeoutReturnsChannel & waitForWithCtxWithTimeoutReturnsChannel | |
// are used bc they return channels(w/ data inside) from other func someTimeConsumingOp | |
// instead of data directly... | |
type someresp struct { | |
status bool | |
} | |
// waitForWithTimeoutReturnsChannel waits for a specific task for a timeout duration. | |
// No context used. | |
// note: the duration must be a bit more than teh expected time the task might take. | |
func waitForWithTimeoutReturnsChannel(timeout time.Duration) (<-chan *someresp, <-chan error) { | |
resC := make(chan *someresp) | |
errC := make(chan error, 1) | |
go func() { | |
defer close(resC) | |
defer close(errC) | |
s, err := someTimeConsumingOp() | |
if err != nil { | |
errC <- err | |
} | |
select { | |
case resC <- &someresp{status: s}: | |
case <-time.After(timeout): | |
err = fmt.Errorf("timeout occurred") | |
errC <- err | |
} | |
}() | |
return resC, errC | |
} | |
// waitForWithCtxWithTimeoutReturnsChannel waits for a specific task till the context is "Done". | |
// note: the invoker must program the context accordingly. | |
func waitForWithCtxWithTimeoutReturnsChannel(ctx context.Context) (<-chan *someresp, <-chan error) { | |
resC := make(chan *someresp) | |
errC := make(chan error, 1) | |
go func() { | |
defer close(resC) | |
defer close(errC) | |
s, err := someTimeConsumingOp() | |
select { | |
case <-ctx.Done(): | |
if ctx.Err() == context.DeadlineExceeded { | |
errC <- fmt.Errorf("canceled due to timeout: %v", ctx.Err()) | |
} else { | |
errC <- fmt.Errorf("canceled: %v", ctx.Err()) | |
} | |
default: | |
if err != nil { | |
errC <- err | |
} else { | |
resC <- &someresp{status: s} | |
} | |
} | |
}() | |
return resC, errC | |
} | |
// waitForWithTimeout waits for a timeout. | |
// note: the invoker must pass teh right timeout. | |
func waitForWithTimeout(timeout time.Duration) (*someresp, error) { | |
// acheive the timeout by creating a context w/ timeout. | |
// or, use time.After(timeout) as waitForWithTimeoutReturnsChannel instead. | |
// Both timeout and context with timeout can be used to enforce time limits for operations, | |
// context with timeout provides additional features such as cancellation propagation and cleanup, | |
// making it more suitable for managing deadlines and timeouts in concurrent programs. | |
ctx, cancel := context.WithTimeout(context.Background(), timeout) | |
defer cancel() | |
s, err := someTimeConsumingOp() | |
select { | |
case <-ctx.Done(): | |
if ctx.Err() == context.DeadlineExceeded { | |
return nil, fmt.Errorf("canceled due to timeout: %v", ctx.Err()) | |
} else { | |
return nil, fmt.Errorf("canceled: %v", ctx.Err()) | |
} | |
default: | |
if err != nil { | |
return nil, err | |
} else { | |
return &someresp{status: s}, nil | |
} | |
} | |
} | |
type Todo struct { | |
UserID int `json:"userId"` | |
ID int `json:"id"` | |
Title string `json:"title"` | |
Body string `json:"body"` | |
} | |
// waitForHttpGetCallWithTimeout shows how a GET http call can be managed and handled via timeouts. | |
func waitForHttpGetCallWithTimeout(url string, timeout time.Duration) (*Todo, error) { | |
// creating a new context with a timeout | |
ctx, cancel := context.WithTimeout(context.Background(), timeout) | |
defer cancel() | |
req, err := http.NewRequest(http.MethodGet, url, nil) | |
if err != nil { | |
return nil, err | |
} | |
// adds the ctx with timeout into req | |
req = req.WithContext(ctx) | |
resp, err := http.DefaultClient.Do(req) | |
if err != nil { | |
select { | |
case <-ctx.Done(): | |
return nil, ctx.Err() | |
default: | |
} | |
return nil, err | |
} | |
defer resp.Body.Close() | |
data := new(Todo) | |
err = json.NewDecoder(resp.Body).Decode(data) | |
if err != nil { | |
return nil, fmt.Errorf("error decoding response body: %v", err) | |
} | |
return data, nil | |
} | |
func main() { | |
if len(os.Args) == 1 { | |
fmt.Println("oops, say 1, 2 or 3!") | |
return | |
} | |
option := os.Args[1] | |
switch option { | |
case "1": | |
// with timeout and receive channels | |
resC, errC := waitForWithTimeoutReturnsChannel(10 * time.Second) | |
select { | |
case result := <-resC: | |
fmt.Println("result:", result) | |
case err := <-errC: | |
fmt.Println("error:", err) | |
} | |
case "2": | |
// with context and receive channels | |
// create context w/ cancel | |
// w/ cancel, ctx gets done once the ops is complete | |
// ctx, cancel := context.WithCancel(context.Background()) | |
// create context w/ timeout | |
// w/ timeout, ctx gets done once the time is complete. | |
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | |
defer cancel() | |
resC, errC := waitForWithCtxWithTimeoutReturnsChannel(ctx) | |
select { | |
case result := <-resC: | |
fmt.Println("result:", result) | |
case err := <-errC: | |
fmt.Println("error:", err) | |
} | |
case "3": | |
res, err := waitForWithTimeout(10 * time.Second) | |
fmt.Println("result:", res, "error:", err) | |
case "4": | |
res, err := waitForHttpGetCallWithTimeout("https://jsonplaceholder.typicode.com/todos/1", 10*time.Second) | |
fmt.Println("result:", res, "error:", err) | |
} | |
} | |
// calling a REST API | |
// or waiting for something to happen | |
// or a task to get to a desired state | |
// (adjust someTimeConsumingOp() signature accordingly) | |
func someTimeConsumingOp() (bool, error) { | |
time.Sleep(5 * time.Second) // some actual op as defined above | |
return true, nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Closing channels
In Go, it's generally not necessary to manually close channels unless you have a specific reason for doing so. Channels are automatically closed when the sender finishes sending values - the channel will be automatically closed when the function returns or when the goroutine sending on the channel completes.