Skip to content

[Enhancement]: Rework background tasks/timers #49

@jonbarrow

Description

@jonbarrow

Checked Existing

  • I have checked the repository for duplicate issues.

What enhancement would you like to see?

(Using the new issue templates flow for this one)

Tagging @DaniElectra for thoughts.

@PabloMK7 has reported that there is another memory leak potentially somewhere in here (the CTGP-7 server apparently crashed from an OOM error). While we have not been able to duplicate the issue, it's still something to look into.

The only real place I can think of that this would happen with our usage of timers and tickers in PRUDPConnection and PendingPacket in the resend scheduler. Timers and tickers can leave resources unfreed and are somewhat annoying to manage between structs and goroutines, making them prone to memory leaks. #44 was made to address this, and seemed to work, but it's possible this didn't catch everything.

We can remove this reliance on tickers/timers entirely by moving to a context based model. This would also be the more "Go" way of doing things (the timer/ticker pattern was carried over from v1 of this module). The context package provides ways to handle tasks that can be canceled and timed out.

Some good reading on this pattern:

A basic implementation using this pattern could look something like:

package main

import (
	"context"
	"fmt"
	"time"
)

type Packet struct {
	data string
}

type ResendTask struct {
	ctx            context.Context
	cancel         context.CancelFunc
	resendCount    int
	maxResends     int
	timeoutSeconds time.Duration
	packet         *Packet
}

func (rt *ResendTask) Begin() {
	if rt.resendCount <= rt.maxResends {
		ctx, cancel := context.WithTimeout(context.Background(), rt.timeoutSeconds*time.Second)

		rt.ctx = ctx
		rt.cancel = cancel

		go rt.start()
	} else {
		fmt.Println("Resent too many times")
	}
}

func (rt *ResendTask) Stop() {
	rt.cancel()
}

func (rt *ResendTask) start() {
	for {
		select {
		case <-rt.ctx.Done():
			fmt.Printf("Timeout. Resending %+v\n", rt.packet)
			rt.resendCount++
			rt.Begin() // * Start again
			return
		}
	}
}

func NewResendTask() *ResendTask {
	return &ResendTask{
		maxResends:     5,
		timeoutSeconds: 5,
		packet: &Packet{
			data: "Payload",
		},
	}
}

func main() {
	task := NewResendTask()

	task.Begin()

	for {
	}
}

In this example:

  • A ResendTask struct is created to manage the retransmission of a packet.
  • Every time ResendTask.Begin is called, a new Background context is created and used with a WithTimeout context.
  • The ResendTask waits to see if the context's timeout elapses, and if so it calls Begin again to create a new timeout context, repeating until the max amount of resends is reached.

This pattern makes managing these timeout/resends easier and less error prone due to 2 key factors of contexts using WithTimeout:

  1. WithTimeout has a cancel function, but unlike WithCancel the Done channel closes and resources are freed once the timeout elapses.
  2. Calling the cancel function from a WithTimeout context will immediately free the resources associated with the context and stop all operations using the context (stopping the timeout check).

Because of these 2 properties we essentially do not need to worry about memory usage for packet retransmission. Whenever a timeout occurs, the context frees itself. If a packet is acknowledged and the context canceled then the resources are also freed.

Any other details to share? (OPTIONAL)

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    awaiting-approvalTopic has not been approved or deniedenhancementAn update to an existing part of the codebase

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions