Skip to content

Commit

Permalink
feat: finish most of the works
Browse files Browse the repository at this point in the history
  • Loading branch information
RexSkz committed Nov 20, 2023
1 parent 9ca426b commit 302edaf
Show file tree
Hide file tree
Showing 10 changed files with 409 additions and 1 deletion.
27 changes: 27 additions & 0 deletions .github/workflows/test.yml
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
coverage.html

# Dependency directories (remove the comment below to include it)
# vendor/
Expand Down
69 changes: 68 additions & 1 deletion README.md
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
31 changes: 31 additions & 0 deletions allsettled.go
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
}
}
16 changes: 16 additions & 0 deletions constant.go
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")
5 changes: 5 additions & 0 deletions go.mod
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
2 changes: 2 additions & 0 deletions go.sum
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=
87 changes: 87 additions & 0 deletions gromise.go
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
}
Loading

0 comments on commit 302edaf

Please sign in to comment.