Skip to content

take into account ctx.Done() when running timers - #7

Open
titivermeesch wants to merge 1 commit into
kuoruan:masterfrom
titivermeesch:fix/cancel-timers-context
Open

take into account ctx.Done() when running timers#7
titivermeesch wants to merge 1 commit into
kuoruan:masterfrom
titivermeesch:fix/cancel-timers-context

Conversation

@titivermeesch

Copy link
Copy Markdown

When running timers I noticed that when the context gets disposed, the timers are not cancelled, causing nil pointer issues because the timer tries to execute inside a context that does not exist anymore.

This makes sure that the timers get cancelled when the context is marked as done.

Breaking changes

This PR changes the signature of the timers InjectTo function, which breaks current implementations of this function.

More information

I looking into using the v8go context, but this does not provide the correct information we need to get this to work

@titivermeesch

Copy link
Copy Markdown
Author

@kuoruan FYI

@kuoruan
kuoruan requested a review from Copilot May 25, 2026 07:38
@kuoruan

kuoruan commented May 25, 2026

Copy link
Copy Markdown
Owner

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces context support to the timers package, enabling proper cancellation of timer goroutines by passing a context.Context through InjectTo and NewTimers. While the implementation correctly refactors the timer loop to handle context cancellation, the review highlights critical concurrency concerns. Specifically, the Items map and NextItemID counter lack synchronization, and there is a data race on the Done field within the timer items. The feedback suggests implementing a sync.Mutex and using atomic operations to ensure thread safety. Additionally, several comments point out that passing the context as an argument to internal methods is redundant as it is already a member of the timers struct.

Comment thread timers/timers.go
type timers struct {
Items map[int32]*internal.Item
NextItemID int32
ctx context.Context

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The Items map and NextItemID counter are accessed concurrently by the V8 execution thread and timer goroutines (via ClearCB). Go maps are not thread-safe for concurrent read/write operations. You should add a sync.Mutex to the timers struct and use it to protect all accesses to these fields.

	ctx        context.Context
	mu         sync.Mutex

Comment thread timers/internal/item.go
Comment on lines +69 to +71
if t.Done {
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

There is a data race on the t.Done field. It is read here in the timer goroutine but can be modified concurrently by the V8 thread via the Clear() method (e.g., when clearTimeout is called) or by the goroutine itself at line 78. Consider using sync/atomic or a mutex to synchronize access to the Done and Cleared fields.

Comment thread timers/timers.go
Comment on lines +26 to 27
"context"
"errors"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add sync to the imports to support the addition of a mutex for thread-safe map access.

Suggested change
"context"
"errors"
"context"
"errors"
"sync"

Comment thread timers/timers.go
ctx := info.Context()

id, err := t.startNewTimer(info.This(), info.Args(), false)
id, err := t.startNewTimer(t.ctx, info.This(), info.Args(), false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The ctx argument is redundant here because it is already stored in the timers struct as t.ctx.

Suggested change
id, err := t.startNewTimer(t.ctx, info.This(), info.Args(), false)
id, err := t.startNewTimer(info.This(), info.Args(), false)

Comment thread timers/timers.go
ctx := info.Context()

id, err := t.startNewTimer(info.This(), info.Args(), true)
id, err := t.startNewTimer(t.ctx, info.This(), info.Args(), true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The ctx argument is redundant here because it is already stored in the timers struct as t.ctx.

Suggested change
id, err := t.startNewTimer(t.ctx, info.This(), info.Args(), true)
id, err := t.startNewTimer(info.This(), info.Args(), true)

Comment thread timers/timers.go
}

func (t *timers) startNewTimer(this v8go.Valuer, args []*v8go.Value, interval bool) (int32, error) {
func (t *timers) startNewTimer(ctx context.Context, this v8go.Valuer, args []*v8go.Value, interval bool) (int32, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Remove the redundant ctx parameter and use t.ctx directly within the method.

Suggested change
func (t *timers) startNewTimer(ctx context.Context, this v8go.Valuer, args []*v8go.Value, interval bool) (int32, error) {
func (t *timers) startNewTimer(this v8go.Valuer, args []*v8go.Value, interval bool) (int32, error) {

Comment thread timers/timers.go
t.Items[item.ID] = item

item.Start()
item.Start(ctx)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Use t.ctx directly since the ctx parameter is being removed from the method signature.

Suggested change
item.Start(ctx)
item.Start(t.ctx)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the timers polyfill to stop running timer goroutines once an associated Go context.Context is done, preventing timers from firing after the embedding environment has been torn down.

Changes:

  • Add context.Context plumbing through timers.InjectTo, NewTimers, and Item.Start so timers can stop on ctx.Done().
  • Update timer execution loop to select on ctx.Done() in addition to ticker events.
  • Store the provided context on the timers instance and pass it into newly created timer items.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
timers/timers.go Threads a Go context through timer creation and start logic.
timers/internal/item.go Updates the timer loop to exit on ctx.Done() and changes Start signature.
timers/inject.go Changes injection API to accept a Go context and constructs timers with it.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread timers/inject.go

func InjectTo(iso *v8go.Isolate, global *v8go.ObjectTemplate) error {
t := NewTimers()
func InjectTo(ctx context.Context, iso *v8go.Isolate, global *v8go.ObjectTemplate) error {
Comment thread timers/inject.go

func InjectTo(iso *v8go.Isolate, global *v8go.ObjectTemplate) error {
t := NewTimers()
func InjectTo(ctx context.Context, iso *v8go.Isolate, global *v8go.ObjectTemplate) error {
Comment thread timers/timers.go
Comment on lines +49 to 54
func NewTimers(ctx context.Context) Timers {
return &timers{
Items: make(map[int32]*internal.Item),
NextItemID: initNextItemID,
ctx: ctx,
}
Comment thread timers/timers.go
Comment on lines 155 to 159
t.NextItemID++
t.Items[item.ID] = item

item.Start()
item.Start(ctx)

Comment thread timers/internal/item.go
Comment on lines +57 to +67
func (t *Item) Start(ctx context.Context) {
go func() {
defer t.Clear() // self clear

ticker := time.NewTicker(time.Duration(t.Delay) * time.Millisecond)
defer ticker.Stop()

for range ticker.C {
if t.Done {
break
}
for {
select {
case <-ctx.Done():
return
Comment thread timers/internal/item.go
Comment on lines +64 to +69
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if t.Done {
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.

3 participants