-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
409 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
name: Unit Test | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
strategy: | ||
matrix: | ||
go-version: ['1.17', '1.18', '1.19', '1.20', '1.21'] | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- name: Setup Go ${{ matrix.go-version }} | ||
uses: actions/setup-go@v4 | ||
with: | ||
go-version: ${{ matrix.go-version }} | ||
- name: Display Go version | ||
run: go version | ||
- name: Install dependencies | ||
run: go mod tidy | ||
- name: Run Tests | ||
run: | | ||
chmod +x ./scripts/run_tests.sh | ||
./scripts/run_tests.sh |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,69 @@ | ||
# gromise | ||
A library to execute goroutines like Promise.all in JavaScript, make some scenarios (e.g. BFF data aggregation) easier. | ||
|
||
A library to execute goroutines like `Promise.allSettled` in JavaScript, make some scenarios (e.g. BFF data aggregation) easier. | ||
|
||
## Usage | ||
|
||
```go | ||
import ( | ||
"errors" | ||
"github.com/rexskz/gromise" | ||
) | ||
|
||
// 1. define a list of functions | ||
fns := []func() (interface{}, error){ | ||
func() (interface{}, error) { | ||
// business logic that may cause lots of time | ||
return data, nil | ||
}, | ||
func() (interface{}, error) { | ||
// a function that can return error | ||
return nil, errors.New("this is an error") | ||
}, | ||
func() (interface{}, error) { | ||
// panic will be converted to error | ||
panic("fatal error") | ||
}, | ||
// ...other functions | ||
} | ||
|
||
// 2. execute all functions concurrently and block until all | ||
// functions are finished, throw error, panic, or timeout | ||
timeoutMs := 1000 | ||
results, err := gromise.New(timeoutMs).AllSettled(fns).Await() | ||
|
||
// 3. the results (if not timeout) will be like: | ||
assert.Equal(t, results, []*gromise.AllSettledValue{ | ||
{ | ||
Status: gromise.StatusFulfilled, | ||
Value: data, | ||
}, | ||
{ | ||
Status: gromise.StatusRejected, | ||
Reason: errors.New("this is an error"), | ||
}, | ||
{ | ||
Status: gromise.StatusRejected, | ||
Reason: errors.New("fatal error"), | ||
}, | ||
}) | ||
|
||
// 4. if timeout, the error will be: | ||
assert.Equal(t, err, gromise.ErrTimeout) | ||
``` | ||
|
||
## Test & Coverage | ||
|
||
```bash | ||
./scripts/run_tests.sh | ||
``` | ||
|
||
You can find the coverage report in `./coverage/index.html`. | ||
|
||
## Why It's Useful | ||
|
||
Sometimes we need to execute a list of functions concurrently and block until all functions are finished, e.g. BFF data aggregation. In JavaScript, we can use `Promise.all` to achieve this, but in Go, we have to use `sync.WaitGroup` or `chan`, and handle the `panic` manually, which is not so convenient. This library is to make this scenario easier. | ||
|
||
## License | ||
|
||
MIT |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package gromise | ||
|
||
type AllSettledValue struct { | ||
Status GromiseStatus | ||
Value interface{} | ||
Reason error | ||
} | ||
|
||
type AllSettledResult struct { | ||
finished chan bool | ||
timeout chan bool | ||
values []*AllSettledValue | ||
} | ||
|
||
func newAllSettledResult() *AllSettledResult { | ||
result := &AllSettledResult{ | ||
finished: make(chan bool), | ||
timeout: make(chan bool), | ||
values: []*AllSettledValue{}, | ||
} | ||
return result | ||
} | ||
|
||
func (r *AllSettledResult) Await() ([]*AllSettledValue, error) { | ||
select { | ||
case <-r.finished: | ||
return r.values, nil | ||
case <-r.timeout: | ||
return r.values, ErrorTimeout | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package gromise | ||
|
||
import ( | ||
"errors" | ||
) | ||
|
||
type GromiseStatus string | ||
|
||
const ( | ||
StatusPending GromiseStatus = "pending" | ||
StatusFulfilled GromiseStatus = "fulfilled" | ||
StatusRejected GromiseStatus = "rejected" | ||
) | ||
|
||
var ErrorTimeout = errors.New("gromise: timeout") | ||
var ErrorUnknownPanic = errors.New("unknown panic") |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module github.com/rexskz/gromise | ||
|
||
go 1.17 | ||
|
||
require gopkg.in/go-playground/assert.v1 v1.2.1 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= | ||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package gromise | ||
|
||
import ( | ||
"errors" | ||
"time" | ||
) | ||
|
||
type Gromise struct { | ||
// TODO: implement max concurrency | ||
// The default value is `runtime.NumCPU()`. | ||
// maxConcurrency int | ||
|
||
timeoutMs int | ||
} | ||
|
||
func New(timeoutMs int) *Gromise { | ||
return &Gromise{ | ||
// maxConcurrency: runtime.NumCPU(), | ||
timeoutMs: timeoutMs, | ||
} | ||
} | ||
|
||
// func (g *Gromise) SetMaxConcurrency(n int) error { | ||
// g.maxConcurrency = n | ||
// return nil | ||
// } | ||
|
||
func (g *Gromise) AllSettled(fns []func() (interface{}, error)) *AllSettledResult { | ||
result := newAllSettledResult() | ||
|
||
go func() { | ||
if len(fns) == 0 { | ||
result.finished <- true | ||
return | ||
} | ||
|
||
goroutinesToWait := len(fns) | ||
now := time.Now() | ||
|
||
result.values = make([]*AllSettledValue, len(fns)) | ||
for index := range result.values { | ||
result.values[index] = &AllSettledValue{ | ||
Status: StatusPending, | ||
} | ||
} | ||
|
||
for index, fn := range fns { | ||
go func(fn func() (interface{}, error), index int) { | ||
defer func() { | ||
goroutinesToWait-- | ||
if r := recover(); r != nil { | ||
result.values[index].Status = StatusRejected | ||
switch x := r.(type) { | ||
case string: | ||
result.values[index].Reason = errors.New(x) | ||
case error: | ||
result.values[index].Reason = x | ||
default: | ||
result.values[index].Reason = ErrorUnknownPanic | ||
} | ||
} | ||
}() | ||
|
||
if r, err := fn(); err != nil { | ||
result.values[index].Status = StatusRejected | ||
result.values[index].Reason = err | ||
} else { | ||
result.values[index].Status = StatusFulfilled | ||
result.values[index].Value = r | ||
} | ||
}(fn, index) | ||
} | ||
|
||
for { | ||
if goroutinesToWait <= 0 { | ||
result.finished <- true | ||
break | ||
} | ||
if time.Since(now).Milliseconds() > int64(g.timeoutMs) { | ||
result.timeout <- true | ||
break | ||
} | ||
} | ||
}() | ||
|
||
return result | ||
} |
Oops, something went wrong.