-
-
Notifications
You must be signed in to change notification settings - Fork 13
fix: Fix race condition #248
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
Conversation
When a process running in a separate go-routine, e.g., a fetch request is represented by a promise in JS, the result of the process is enqueued on the task queue of the main go routine. However, converting the result from a native Go value to a JavaScript value should have occurred in the main go-routine, not the concurrent task. This could cause concurrent calls to the same JavaScript context, which isn't allowed.
WalkthroughThis PR introduces a scoped clock execution model: Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
| func (c *Clock) enter() { c.stack++ } | ||
| func (c *Clock) exit() error { | ||
| c.stack-- | ||
| if c.stack > 0 { | ||
| return nil | ||
| } | ||
| return c.tick() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding a defensive check for unbalanced calls.
If exit() is ever called without a matching enter() (e.g., due to a future refactoring mistake), stack would become negative and tick() would still run. While the current code correctly pairs these via Do(), a defensive check could help catch bugs early during development.
🛡️ Optional: Add defensive assertion
func (c *Clock) enter() { c.stack++ }
func (c *Clock) exit() error {
c.stack--
+ if c.stack < 0 {
+ panic("gost-dom/clock: exit() called without matching enter()")
+ }
if c.stack > 0 {
return nil
}
return c.tick()
}📝 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.
| func (c *Clock) enter() { c.stack++ } | |
| func (c *Clock) exit() error { | |
| c.stack-- | |
| if c.stack > 0 { | |
| return nil | |
| } | |
| return c.tick() | |
| } | |
| func (c *Clock) enter() { c.stack++ } | |
| func (c *Clock) exit() error { | |
| c.stack-- | |
| if c.stack < 0 { | |
| panic("gost-dom/clock: exit() called without matching enter()") | |
| } | |
| if c.stack > 0 { | |
| return nil | |
| } | |
| return c.tick() | |
| } |
| // Tick runs all tasks scheduled for immediate execution. This is synonymous | ||
| // with calling Advance(0). | ||
| func (c *Clock) Tick() error { return c.Advance(0) } | ||
| func (c *Clock) tick() error { return c.Advance(0) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Minor: Update comment to reflect the method is now private.
The comment still describes Tick as if it were a public API entry point. Since it's now private (tick), consider updating the comment to reflect its internal nature, or removing it since callers should use Do() instead.
📝 Suggested comment update
-// Tick runs all tasks scheduled for immediate execution. This is synonymous
-// with calling Advance(0).
+// tick runs all tasks scheduled for immediate execution (equivalent to Advance(0)).
+// This is called internally by exit() when the outermost Do() completes.
func (c *Clock) tick() error { return c.Advance(0) }| return m.ctx.do(func() error { | ||
| m.ctx.logger().Debug("Evaluate module", "vm", m.ctx.vm) | ||
| p := m.record.Evaluate(m.ctx.vm) | ||
|
|
||
| if p.State() != sobek.PromiseStateFulfilled { | ||
| return p.Result().Export().(error) | ||
| } | ||
| return m.ctx.tick() | ||
| if p.State() != sobek.PromiseStateFulfilled { | ||
| res := p.Result().Export() | ||
| err, ok := res.(error) | ||
| if !ok { | ||
| err = fmt.Errorf("run module: %v", res) | ||
| } | ||
| return err | ||
| } | ||
| return nil | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, locate the file and understand the repository structure
find . -name "sobek_module.go" -type f | head -20Repository: gost-dom/browser
Length of output: 99
🏁 Script executed:
#!/bin/bash
# List relevant files in the scripting/sobekengine directory
git ls-files | grep -E "(sobek|script)" | head -30Repository: gost-dom/browser
Length of output: 1678
🏁 Script executed:
#!/bin/bash
# Inspect the sobek_module.go file around lines 20-33
find . -name "sobek_module.go" -type f -exec cat -n {} \; | head -50Repository: gost-dom/browser
Length of output: 907
🏁 Script executed:
#!/bin/bash
# Find the scriptContext type and its do() method
rg -n "func.*\(.*scriptContext.*\)\s*do\(" scripting/sobekengine/ -A 10Repository: gost-dom/browser
Length of output: 859
🏁 Script executed:
#!/bin/bash
# Search for scriptContext definition
rg -n "type scriptContext" scripting/sobekengine/ -A 20Repository: gost-dom/browser
Length of output: 1530
🏁 Script executed:
#!/bin/bash
# Search for Clock.Do method or related event loop handling
rg -n "Clock|EventLoop|RunTicks|Drain" scripting/sobekengine/ -A 5 -B 2Repository: gost-dom/browser
Length of output: 1617
🏁 Script executed:
#!/bin/bash
# Look for all go files in sobekengine to understand the structure
git ls-files scripting/sobekengine/ | grep "\.go$"Repository: gost-dom/browser
Length of output: 668
🏁 Script executed:
#!/bin/bash
# Find the Clock type and its Do method implementation
find . -name "clock.go" -o -name "clock*.go" | grep -v test | xargs cat -n 2>/dev/null | head -100Repository: gost-dom/browser
Length of output: 4604
🏁 Script executed:
#!/bin/bash
# Search for Clock.Do method definition across the codebase
rg -n "func.*Clock.*Do\(|func \(c \*Clock\)" --type=go -A 15Repository: gost-dom/browser
Length of output: 15141
🏁 Script executed:
#!/bin/bash
# Look for the clock package
git ls-files | grep clock | grep -v test | grep "\.go$"Repository: gost-dom/browser
Length of output: 83
🏁 Script executed:
#!/bin/bash
# Search for tests or usage patterns involving module evaluation
rg -n "sobekModule|Run\(\)|Evaluate.*module" --type=go -B 2 -A 3 | head -60Repository: gost-dom/browser
Length of output: 3260
🏁 Script executed:
#!/bin/bash
# Look for any handling of PromiseStatePending or promise settlement patterns
rg -n "PromiseState|PromisePending|moduleRecord\.Evaluate" --type=go -B 2 -A 2Repository: gost-dom/browser
Length of output: 404
🏁 Script executed:
#!/bin/bash
# Check if there are comments or tests explaining expected module behavior
rg -n "top.?level.*await|module.*settle|async.*module" --type=go -i | head -30Repository: gost-dom/browser
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check for tests related to module evaluation
find . -name "*sobek*test.go" -o -name "*module*test.go" | xargs grep -l "module\|Run\|Evaluate" 2>/dev/nullRepository: gost-dom/browser
Length of output: 133
🏁 Script executed:
#!/bin/bash
# Look at the sobek_test.go file to see how modules are tested
cat -n scripting/sobekengine/sobek_test.go | head -100Repository: gost-dom/browser
Length of output: 1314
🏁 Script executed:
#!/bin/bash
# Search for any comments or documentation about module expectations in sobek_module.go
cat -n scripting/sobekengine/sobek_module.goRepository: gost-dom/browser
Length of output: 907
🏁 Script executed:
#!/bin/bash
# Check if Sobek documentation or examples mention promise settlement
rg -n "Evaluate|Promise|module" scripting/sobekengine/ --type=go | grep -i "comment\|doc\|test" | head -20Repository: gost-dom/browser
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Look at scripttests to understand what modules are being tested
find . -path "*/scripttests*" -name "*.go" | head -10Repository: gost-dom/browser
Length of output: 592
🏁 Script executed:
#!/bin/bash
# Check the module test suite to see what module behavior is expected
rg -n "RunModuleSuite|TestESM" scripting/internal/ --type=go -A 5Repository: gost-dom/browser
Length of output: 625
🏁 Script executed:
#!/bin/bash
# Search for any top-level await or async module handling in tests
rg -n "await|async|top.?level" scripting/internal/ --type=go | head -20Repository: gost-dom/browser
Length of output: 701
🏁 Script executed:
#!/bin/bash
# Get the full module test suite to see what scenarios are tested
cat -n scripting/internal/scripttests/modules_test_suite.go | head -200Repository: gost-dom/browser
Length of output: 6960
🏁 Script executed:
#!/bin/bash
# Check if sobek.ModuleRecord.Evaluate ever returns a pending promise, or if it always settles synchronously
rg -n "ModuleRecord\|\.Evaluate" scripting/sobekengine/ --type=go -B 2 -A 5Repository: gost-dom/browser
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Look for references to Sobek library or documentation about module evaluation
rg -n "github.com/grafana/sobek" . --type=go | grep -i "doc\|comment\|import" | head -10Repository: gost-dom/browser
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Search for any documentation or issues about async modules or top-level await
rg -n "top.*level|async.*module|awaiting.*module" . --type=go --type=md | head -20Repository: gost-dom/browser
Length of output: 161
🏁 Script executed:
#!/bin/bash
# Check V8 module implementation for comparison
cat -n scripting/v8engine/module.go | head -50Repository: gost-dom/browser
Length of output: 1476
🏁 Script executed:
#!/bin/bash
# See the full awaitPromise implementation in V8
cat -n scripting/v8engine/module.goRepository: gost-dom/browser
Length of output: 2421
🏁 Script executed:
#!/bin/bash
# Check if there's a similar awaiting mechanism for Sobek
rg -n "awaitPromise|await.*Promise|promise.*settle" scripting/sobekengine/ --type=go -A 5Repository: gost-dom/browser
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Look at how Sobek handles promises in general
rg -n "Promise|promise" scripting/sobekengine/ --type=go | head -30Repository: gost-dom/browser
Length of output: 1121
🏁 Script executed:
#!/bin/bash
# Check Sobek documentation/issues about module evaluation and promise states
rg -n "PromiseState|Fulfilled|Pending|Rejected" scripting/sobekengine/ --type=goRepository: gost-dom/browser
Length of output: 148
🏁 Script executed:
#!/bin/bash
# Verify if there's any mechanism in Sobek to wait for promises like V8's PerformMicrotaskCheckpoint
rg -n "Microtask|checkpoint|drain" scripting/sobekengine/ --type=goRepository: gost-dom/browser
Length of output: 42
Pending promise incorrectly treated as error—module evaluation does not await promise settlement.
The promise state is checked synchronously within Clock.Do() before microtasks are drained. For modules with top-level await (which return a pending promise), the check at line 24 will treat PromiseStatePending as a failure.
Compare to the V8 implementation (scripting/v8engine/module.go), which explicitly awaits the promise via awaitPromise() to allow settlement before returning. The Sobek implementation lacks this promise-awaiting mechanism and will reject async modules.
Recommend adding explicit promise settlement logic similar to V8, or ensuring Clock.Do() drains microtasks before the state check occurs.
When a process running in a separate go-routine, e.g., a fetch request is represented by a promise in JS, the result of the process is enqueued on the task queue of the main go routine.
However, converting the result from a native Go value to a JavaScript value should have occurred in the main go-routine, not the concurrent task. This could cause concurrent calls to the same JavaScript context, which isn't allowed.