Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new features finally, race, timeout #7

Merged
merged 1 commit into from
Sep 22, 2024

Conversation

hoanguyenkh
Copy link
Owner

@hoanguyenkh hoanguyenkh commented Sep 22, 2024

Summary by CodeRabbit

  • New Features

    • Introduced new functions for promise handling: Race, Finally, and Timeout.
    • Enhanced functionality for concurrent promise resolution.
  • Bug Fixes

    • Improved thread safety in promise handling.
  • Tests

    • Added comprehensive tests for new promise functionalities, including race conditions, final execution behavior, and timeout handling.

Copy link

coderabbitai bot commented Sep 22, 2024

Walkthrough

The changes involve significant modifications to the promise handling functionality in the codebase. The README.md file reflects updates in promise instantiation by removing the context parameter. The promise.go file introduces thread-safe promise management using atomic.Value, a new completion signaling mechanism, and additional methods for promise handling, including Race, Finally, and Timeout. The promise_test.go file expands the test coverage with new test cases for racing promises, final execution behavior, and timeout handling.

Changes

File Change Summary
README.md Removed context parameter from promise instantiation in the main function.
promise.go Modified Promise struct to use atomic.Value, introduced done channel, refactored All and AllWithPool using sync.WaitGroup, added Race, Finally, and Timeout methods, and updated method signatures with comments.
promise_test.go Added new test functions: TestPromise_Race, TestPromise_Finally, and TestPromise_Timeout with sub-tests for various promise behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Promise
    participant Resolver
    participant Rejector

    Caller->>Promise: Create Promise
    Promise->>Resolver: Start resolving
    Promise->>Rejector: Handle rejection
    Resolver-->>Promise: Resolve value
    Rejector-->>Promise: Reject error
    Promise-->>Caller: Return resolved value or error
Loading

🐰 In the meadow, promises bloom,
With atomic care, they dispel the gloom.
Racing fast, they resolve with grace,
Finally, they finish, no time to waste!
Timeout whispers, "Hurry, don't delay!"
A rabbit's cheer for promises at play! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share
Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    -- I pushed a fix in commit <commit_id>, please review it.
    -- Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    -- @coderabbitai generate unit testing code for this file.
    -- @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    -- @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    -- @coderabbitai read src/utils.ts and generate unit testing code.
    -- @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    -- @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@hoanguyenkh hoanguyenkh merged commit 16ff1a8 into main Sep 22, 2024
2 of 3 checks passed
@hoanguyenkh hoanguyenkh deleted the hoa/add-finaly-race-timeout branch September 22, 2024 02:18
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Outside diff range and nitpick comments (3)
promise_test.go (3)

365-404: Comprehensive testing of race conditions.

The TestPromise_Race function effectively tests both the fastest resolve and reject scenarios in race conditions. The use of require.NoError and require.Equal in RaceWithFastestResolve ensures that the correct promise wins the race, and similar assertions in RaceWithFastestReject validate the error handling.

Consider adding a brief comment above each sub-test to explain the setup and expected outcome for future maintainability.


407-439: Effective testing of finally block execution.

The TestPromise_Finally function correctly tests the execution of the finally block in both successful and error scenarios. The use of require.NoError and require.True in FinallyAfterResolve confirms that the finally block executes after a successful resolution. Similarly, require.Error and require.True in FinallyAfterReject ensure that the finally block executes despite the promise rejection.

Adding comments to explain the purpose of setting finallyExecuted and its importance in the test could enhance readability and maintainability.


442-464: Accurate testing of timeout behavior.

The TestPromise_Timeout function effectively tests the timeout behavior of promises. The sub-test TimeoutBeforeResolve correctly asserts an error when the promise resolution exceeds the specified timeout, using require.Error. The sub-test ResolveBeforeTimeout confirms that the promise resolves correctly within the timeout period, using require.NoError and require.Equal.

Consider adding more detailed comments to explain the setup and expected outcomes, especially the significance of the timeout values chosen for the tests.

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 513e69f and b27181c.

Files selected for processing (3)
  • README.md (1 hunks)
  • promise.go (4 hunks)
  • promise_test.go (1 hunks)
Additional context used
Markdownlint
README.md

29-29: Column: 1
Hard tabs

(MD010, no-hard-tabs)


30-30: Column: 1
Hard tabs

(MD010, no-hard-tabs)


31-31: Column: 1
Hard tabs

(MD010, no-hard-tabs)


32-32: Column: 1
Hard tabs

(MD010, no-hard-tabs)


34-34: Column: 1
Hard tabs

(MD010, no-hard-tabs)


35-35: Column: 1
Hard tabs

(MD010, no-hard-tabs)


36-36: Column: 1
Hard tabs

(MD010, no-hard-tabs)


37-37: Column: 1
Hard tabs

(MD010, no-hard-tabs)


39-39: Column: 1
Hard tabs

(MD010, no-hard-tabs)

Additional comments not posted (4)
README.md (3)

29-29: Question the removal of ctx from promise instantiation.

The removal of the context parameter from the promise instantiation could significantly alter the behavior of these promises, particularly in scenarios involving cancellation or timeouts. Can you clarify the rationale behind this change? It's crucial to ensure that the promises still support these features if needed.

Tools
Markdownlint

29-29: Column: 1
Hard tabs

(MD010, no-hard-tabs)


34-34: Reiterate concerns regarding the removal of ctx.

As with p1, the removal of context from p2's instantiation needs clarification. This change could impact the promise's ability to handle cancellations and timeouts effectively.

Tools
Markdownlint

34-34: Column: 1
Hard tabs

(MD010, no-hard-tabs)


39-39: Repeat concerns for ctx removal in p3.

The removal of context from the instantiation of p3 mirrors the concerns raised for p1 and p2. It's important to understand the implications of this change on the promise's functionality, especially regarding cancellation and timeout handling.

Tools
Markdownlint

39-39: Column: 1
Hard tabs

(MD010, no-hard-tabs)

promise.go (1)

189-199: Ensure original promises handle context cancellation in Timeout

The Timeout function relies on the original promise (p) respecting context cancellation to enforce the timeout. If p does not handle context cancellation, it may continue executing after the timeout, leading to potential resource leaks. Ensure that all promise tasks accept and correctly handle the context.

Run the following script to check that all promise tasks accept a context parameter:

Comment on lines +177 to +185
return NewWithPool(func(resolve func(T), reject func(error)) {
result, err := p.Await(ctx)
fn()
if err != nil {
reject(err)
} else {
resolve(result)
}
}, defaultPool)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure Finally function handles panics

In the Finally function, if fn() panics, it will propagate up and potentially crash the program. Consider wrapping the call to fn() in a recover block to handle any panics gracefully.

Apply this diff to handle panics in fn():

result, err := p.Await(ctx)
-   fn()
+   func() {
+       defer func() {
+           if r := recover(); r != nil {
+               // Handle or log the panic as necessary
+               reject(fmt.Errorf("panic in Finally function: %v", r))
+           }
+       }()
+       fn()
+   }()
    if err != nil {
        reject(err)
    } else {
        resolve(result)
    }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return NewWithPool(func(resolve func(T), reject func(error)) {
result, err := p.Await(ctx)
fn()
if err != nil {
reject(err)
} else {
resolve(result)
}
}, defaultPool)
return NewWithPool(func(resolve func(T), reject func(error)) {
result, err := p.Await(ctx)
func() {
defer func() {
if r := recover(); r != nil {
// Handle or log the panic as necessary
reject(fmt.Errorf("panic in Finally function: %v", r))
}
}()
fn()
}()
if err != nil {
reject(err)
} else {
resolve(result)
}
}, defaultPool)

Comment on lines +66 to +67
p.err.Store(err)
close(p.done)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check for nil before storing errors in reject method

Storing a nil error in atomic.Value can cause panics during type assertions. Ensure that err is not nil before storing it, or handle nil errors appropriately.

Apply this diff to verify the error:

func (p *Promise[T]) reject(err error) {
    p.once.Do(func() {
+       if err == nil {
+           err = fmt.Errorf("unknown error")
+       }
        p.err.Store(err)
        close(p.done)
    })
}

Committable suggestion was skipped due to low confidence.

Comment on lines +118 to +132
// Race returns a promise that resolves or rejects as soon as one of the promises resolves or rejects
func Race[T any](ctx context.Context, promises ...*Promise[T]) *Promise[T] {
return NewWithPool(func(resolve func(T), reject func(error)) {
for _, p := range promises {
go func(p *Promise[T]) {
result, err := p.Await(ctx)
if err != nil {
reject(err)
} else {
resolve(result)
}
}(p)
}
}, defaultPool)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prevent goroutine leaks in Race function

In the Race function, once the promise is resolved or rejected, the remaining goroutines continue executing, which can lead to unnecessary resource usage. Consider canceling the context to stop other goroutines after the first promise settles.

Apply this diff to add context cancellation:

func Race[T any](ctx context.Context, promises ...*Promise[T]) *Promise[T] {
+   ctx, cancel := context.WithCancel(ctx)
    return NewWithPool(func(resolve func(T), reject func(error)) {
+       defer cancel()
        for _, p := range promises {
            go func(p *Promise[T]) {
+               select {
+               case <-ctx.Done():
+                   return
+               default:
+               }
                result, err := p.Await(ctx)
                if err != nil {
                    reject(err)
+                   cancel()
                } else {
                    resolve(result)
+                   cancel()
                }
            }(p)
        }
    }, defaultPool)
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Race returns a promise that resolves or rejects as soon as one of the promises resolves or rejects
func Race[T any](ctx context.Context, promises ...*Promise[T]) *Promise[T] {
return NewWithPool(func(resolve func(T), reject func(error)) {
for _, p := range promises {
go func(p *Promise[T]) {
result, err := p.Await(ctx)
if err != nil {
reject(err)
} else {
resolve(result)
}
}(p)
}
}, defaultPool)
}
// Race returns a promise that resolves or rejects as soon as one of the promises resolves or rejects
func Race[T any](ctx context.Context, promises ...*Promise[T]) *Promise[T] {
ctx, cancel := context.WithCancel(ctx)
return NewWithPool(func(resolve func(T), reject func(error)) {
defer cancel()
for _, p := range promises {
go func(p *Promise[T]) {
select {
case <-ctx.Done():
return
default:
}
result, err := p.Await(ctx)
if err != nil {
reject(err)
cancel()
} else {
resolve(result)
cancel()
}
}(p)
}
}, defaultPool)
}

Comment on lines +97 to +113
var wg sync.WaitGroup
wg.Add(len(promises))

for i, p := range promises {
i, p := i, p
pool.Go(func() {
defer wg.Done()
result, err := p.Await(ctx)
if err != nil {
reject(err)
return
}
results[i] = result
})
}

wg.Wait()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle early rejection in AllWithPool

In AllWithPool, if one promise rejects, reject(err) is called, but other promises may continue executing, leading to unnecessary processing. Introduce context cancellation to stop processing remaining promises when one fails.

Apply this diff to implement cancellation:

func AllWithPool[T any](ctx context.Context, pool Pool, promises ...*Promise[T]) *Promise[[]T] {
    if len(promises) == 0 {
        panic("missing promises")
    }

+   ctx, cancel := context.WithCancel(ctx)
+   defer cancel()

    return NewWithPool(func(resolve func([]T), reject func(error)) {
        results := make([]T, len(promises))
        var wg sync.WaitGroup
        wg.Add(len(promises))

        for i, p := range promises {
            i, p := i, p
            pool.Go(func() {
                defer wg.Done()
+               select {
+               case <-ctx.Done():
+                   return
+               default:
+               }
                result, err := p.Await(ctx)
                if err != nil {
                    reject(err)
+                   cancel()
                    return
                }
                results[i] = result
            })
        }

        wg.Wait()
        resolve(results)
    }, pool)
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var wg sync.WaitGroup
wg.Add(len(promises))
for i, p := range promises {
i, p := i, p
pool.Go(func() {
defer wg.Done()
result, err := p.Await(ctx)
if err != nil {
reject(err)
return
}
results[i] = result
})
}
wg.Wait()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var wg sync.WaitGroup
wg.Add(len(promises))
for i, p := range promises {
i, p := i, p
pool.Go(func() {
defer wg.Done()
select {
case <-ctx.Done():
return
default:
}
result, err := p.Await(ctx)
if err != nil {
reject(err)
cancel()
return
}
results[i] = result
})
}
wg.Wait()

Comment on lines +49 to +53
if err := p.err.Load(); err != nil {
var t T
return t, err.(error)
}
return p.value.Load().(T), nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safely handle type assertions from atomic.Value

When loading values from atomic.Value, type assertions can panic if the stored value is nil or of an unexpected type. To prevent potential panics, ensure to check for nil and handle type assertions safely using the comma-ok idiom.

Apply this diff to enhance safety:

case <-p.done:
    if errInterface := p.err.Load(); errInterface != nil {
        var t T
-       return t, errInterface.(error)
+       if err, ok := errInterface.(error); ok {
+           return t, err
+       } else {
+           return t, fmt.Errorf("unexpected error type: %T", errInterface)
+       }
    }
    if valueInterface := p.value.Load(); valueInterface != nil {
-       return p.value.Load().(T), nil
+       if value, ok := valueInterface.(T); ok {
+           return value, nil
+       } else {
+           var t T
+           return t, fmt.Errorf("unexpected value type: %T", valueInterface)
+       }
+   } else {
+       var t T
+       return t, fmt.Errorf("value is nil")
    }

Committable suggestion was skipped due to low confidence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant