-
Notifications
You must be signed in to change notification settings - Fork 20
Description
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:
- https://medium.com/@alireza.stack/cancellation-pattern-implementation-in-go-179706ae772b
- https://golangbot.com/context-timeout-cancellation
- https://ayada.dev/posts/background-tasks-in-go
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
ResendTaskstruct is created to manage the retransmission of a packet. - Every time
ResendTask.Beginis called, a newBackgroundcontext is created and used with aWithTimeoutcontext. - The
ResendTaskwaits to see if the context's timeout elapses, and if so it callsBeginagain 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:
WithTimeouthas acancelfunction, but unlikeWithCanceltheDonechannel closes and resources are freed once the timeout elapses.- Calling the
cancelfunction from aWithTimeoutcontext 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
Labels
Type
Projects
Status