Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum
.idea

# env file
.env

.DS_Store
nylon

central.key
central.yaml
node.yaml
central.nybundle

distribute.sh
distribute-conf.sh
log

log.csv

vendor

workdir
Dockerfile
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ log

log.csv

vendor
vendor

workdir
42 changes: 26 additions & 16 deletions polyamide/device/peer.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,31 +135,41 @@ func (peer *Peer) SendBuffers(buffers [][]byte, eps []conn.Endpoint) error {
for _, ep := range endpoints {
ep.ClearSrc()
}
for _, ep := range eps {
if ep != nil {
ep.ClearSrc()
}
}
peer.endpoints.clearSrcOnTx = false
}
peer.endpoints.Unlock()

// group by endpoints
bufByEp := make(map[conn.Endpoint][][]byte)
for i, buf := range buffers {
bufByEp[eps[i]] = append(bufByEp[eps[i]], buf)
}
// optimization, if multiple contiguous buffers share the same endpoint, send them in a single batch
prevIdx := 0
prevEp := eps[0]

Copilot AI Nov 9, 2025

Copy link

Choose a reason for hiding this comment

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

Potential panic if eps slice is empty. Line 149 accesses eps[0] without checking if the slice has any elements. While the current code flow may prevent empty slices from reaching this function, defensive programming would suggest adding a length check or early return for empty inputs.

Copilot uses AI. Check for mistakes.

var totalLen uint64
var anyError error
for ep, bufs := range bufByEp {
if ep == nil {
ep = endpoints[0] // default endpoint
}
err := peer.device.net.bind.Send(bufs, ep)
if err == nil {
for _, b := range bufs {
totalLen += uint64(len(b))
for i := 0; i <= len(buffers); i++ {
if i == len(buffers) || eps[i] != prevEp {
// send batch from prevIdx to i-1
if prevEp == nil {
prevEp = endpoints[0] // default endpoint
}
err := peer.device.net.bind.Send(buffers[prevIdx:i], prevEp)
if err != nil {
anyError = err
}
prevIdx = i
if i < len(buffers) {
prevEp = eps[i]
}
} else {
anyError = err
}
}

var totalLen uint64
for _, b := range buffers {
totalLen += uint64(len(b))
}
peer.txBytes.Add(totalLen)
return anyError
}
Expand Down
59 changes: 58 additions & 1 deletion polyamide/device/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,54 @@ func (device *Device) RoutineEncryption(id int) {
}
}

/*
This function arranges buffers and endpoints by endpoint, into contiguous
segments of endpoints. It ensures that packets within the same endpoint group
are stable-ordered.
*/
func partitionBuffersByEndpoint(bufs [][]byte, eps []conn.Endpoint, outBufs *[][]byte, outEps *[]conn.Endpoint, peer *Peer) error {
// use a naive O(n^2) algorithm since n is expected to be small

// replace nil endpoints with the first known endpoint
peer.endpoints.Lock()
for i := 0; i < len(eps); i++ {
if eps[i] == nil {
if len(peer.endpoints.val) == 0 {
peer.endpoints.Unlock()
return errors.New("no known endpoints for peer")
}
eps[i] = peer.endpoints.val[0]
}
}
peer.endpoints.Unlock()

for i := 0; i < len(bufs); i++ {
// If endpoint is nil, it has already been processed
// as part of an earlier group.
Comment on lines +463 to +464

Copilot AI Nov 9, 2025

Copy link

Choose a reason for hiding this comment

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

The comment "If endpoint is nil, it has already been processed as part of an earlier group" is misleading. At this point in the code, all originally-nil endpoints have been replaced with peer.endpoints.val[0] in the previous loop (lines 451-459). The nil check here is only catching entries that were marked as visited (set to nil) during the current partitioning loop, not originally-nil entries.

Suggested change
// If endpoint is nil, it has already been processed
// as part of an earlier group.
// If endpoint is nil, it was marked as visited during this loop
// and has already been processed as part of an earlier group.

Copilot uses AI. Check for mistakes.
if eps[i] == nil {
continue
}

// This is the first item of a new group.
currentEp := eps[i]
*outBufs = append(*outBufs, bufs[i])
*outEps = append(*outEps, currentEp)
eps[i] = nil // Mark as visited

// Scan the rest of the array for other items in this group
for j := i + 1; j < len(bufs); j++ {
// Check if it's the same endpoint
if eps[j] == currentEp {
// It's part of the same group. Add it.
*outBufs = append(*outBufs, bufs[j])
*outEps = append(*outEps, currentEp)
eps[j] = nil // Mark as visited
}
}
}
return nil
}

func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
device := peer.device
defer func() {
Expand All @@ -447,11 +495,15 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
device.Log.Verbosef("%v - Routine: sequential sender - started", peer)

bufs := make([][]byte, 0, maxBatchSize)
parBufs := make([][]byte, 0, maxBatchSize)
eps := make([]conn.Endpoint, 0)
parEps := make([]conn.Endpoint, 0)

for elemsContainer := range peer.queue.outbound.c {
bufs = bufs[:0]
eps = eps[:0]
parBufs = parBufs[:0]
parEps = parEps[:0]
if elemsContainer == nil {
return
}
Expand Down Expand Up @@ -483,7 +535,12 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
peer.timersAnyAuthenticatedPacketTraversal(false)
peer.timersAnyAuthenticatedPacketSent()

err := peer.SendBuffers(bufs, eps)
err := partitionBuffersByEndpoint(bufs, eps, &parBufs, &parEps, peer)
if err != nil {
device.Log.Errorf("%v - Failed to send data packets: %v", peer, err)
continue
}
Comment on lines +539 to +542

Copilot AI Nov 9, 2025

Copy link

Choose a reason for hiding this comment

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

The lock acquired at line 526 via elemsContainer.Lock() is not released before the continue statement when partitionBuffersByEndpoint returns an error. This will cause a deadlock. The unlock should happen before continuing, or the cleanup code (lines 547-551) should be executed even on error.

Copilot uses AI. Check for mistakes.
err = peer.SendBuffers(parBufs, parEps)
if dataSent {
peer.timersDataSent()
}
Expand Down
4 changes: 3 additions & 1 deletion polyamide/device/traffic_control.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package device

import (
"github.com/encodeous/nylon/polyamide/conn"
"slices"

"github.com/encodeous/nylon/polyamide/conn"
)

// polyamide traffic control provides a facility to re-order, manipulate, and redirect packets between nylon/polyamide nodes
Expand Down Expand Up @@ -200,6 +201,7 @@ func (device *Device) TCBatch(batch []*TCElement, tcs *TCState) {
obe.packet = elem.Packet
obe.buffer = elem.Buffer
obec.elems = append(obec.elems, obe)
device.PutTCElement(elem)
elems[i] = nil
}
peer.StagePackets(obec)
Expand Down
Loading