Files
buildkit/solver/internal/pipe/pipe.go
Jonathan A. Sternberg ad64996f49 solver: pipe implementation utilizes generics for better typing
This updates the pipe library to use generics for the request payload
and the status value. This allows the solver to put in explicit types
rather than rely on type casting from interfaces which helps with type
safety and understandability.

The status value used by the solver uses the `any` type instead of an
explicit type because the `unpark` method takes a generic list of pipes
and the different pipes have different result types. We can likely
change this in the future or create a discriminated union for the
types that can be used in this package. That is left for future work
because at least the request payload is typed now.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
2024-09-12 10:37:12 -05:00

194 lines
3.7 KiB
Go

package pipe
import (
"context"
"sync"
"sync/atomic"
"github.com/pkg/errors"
)
type channel[V any] struct {
OnSendCompletion func()
value atomic.Pointer[V]
lastValue *V
}
func (c *channel[V]) Send(v V) {
c.value.Store(&v)
if c.OnSendCompletion != nil {
c.OnSendCompletion()
}
}
func (c *channel[V]) Receive() (V, bool) {
v := c.value.Load()
if v == nil || v == c.lastValue {
return *new(V), false
}
c.lastValue = v
return *v, true
}
type Pipe[Payload, Value any] struct {
Sender Sender[Payload, Value]
Receiver Receiver[Payload, Value]
OnReceiveCompletion func()
OnSendCompletion func()
}
type Request[Payload any] struct {
Payload Payload
Canceled bool
}
type Sender[Payload, Value any] interface {
Request() Request[Payload]
Update(v Value)
Finalize(v Value, err error)
Status() Status[Value]
}
type Receiver[Payload, Value any] interface {
Receive() bool
Cancel()
Status() Status[Value]
Request() Payload
}
type Status[Value any] struct {
Canceled bool
Completed bool
Err error
Value Value
}
func NewWithFunction[Payload, Value any](f func(context.Context) (Value, error)) (*Pipe[Payload, Value], func()) {
p := New[Payload, Value](Request[Payload]{})
ctx, cancel := context.WithCancelCause(context.TODO())
p.OnReceiveCompletion = func() {
if req := p.Sender.Request(); req.Canceled {
cancel(errors.WithStack(context.Canceled))
}
}
return p, func() {
res, err := f(ctx)
if err != nil {
p.Sender.Finalize(*new(Value), err)
return
}
p.Sender.Finalize(res, nil)
}
}
func New[Payload, Value any](req Request[Payload]) *Pipe[Payload, Value] {
cancelCh := &channel[Request[Payload]]{}
roundTripCh := &channel[Status[Value]]{}
pw := &sender[Payload, Value]{
req: req,
sendChannel: roundTripCh,
}
pr := &receiver[Payload, Value]{
req: req,
recvChannel: roundTripCh,
sendChannel: cancelCh,
}
p := &Pipe[Payload, Value]{
Sender: pw,
Receiver: pr,
}
cancelCh.OnSendCompletion = func() {
v, ok := cancelCh.Receive()
if ok {
pw.setRequest(v)
}
if p.OnReceiveCompletion != nil {
p.OnReceiveCompletion()
}
}
roundTripCh.OnSendCompletion = func() {
if p.OnSendCompletion != nil {
p.OnSendCompletion()
}
}
return p
}
type sender[Payload, Value any] struct {
status Status[Value]
req Request[Payload]
sendChannel *channel[Status[Value]]
mu sync.Mutex
}
func (pw *sender[Payload, Value]) Status() Status[Value] {
return pw.status
}
func (pw *sender[Payload, Value]) Request() Request[Payload] {
pw.mu.Lock()
defer pw.mu.Unlock()
return pw.req
}
func (pw *sender[Payload, Value]) setRequest(req Request[Payload]) {
pw.mu.Lock()
defer pw.mu.Unlock()
pw.req = req
}
func (pw *sender[Payload, Value]) Update(v Value) {
pw.status.Value = v
pw.sendChannel.Send(pw.status)
}
func (pw *sender[Payload, Value]) Finalize(v Value, err error) {
pw.status.Value = v
pw.status.Err = err
pw.status.Completed = true
if errors.Is(err, context.Canceled) && pw.req.Canceled {
pw.status.Canceled = true
}
pw.sendChannel.Send(pw.status)
}
type receiver[Payload, Value any] struct {
status Status[Value]
req Request[Payload]
recvChannel *channel[Status[Value]]
sendChannel *channel[Request[Payload]]
}
func (pr *receiver[Payload, Value]) Request() Payload {
return pr.req.Payload
}
func (pr *receiver[Payload, Value]) Receive() bool {
v, ok := pr.recvChannel.Receive()
if !ok {
return false
}
pr.status = v
return true
}
func (pr *receiver[Payload, Value]) Cancel() {
req := pr.req
if req.Canceled {
return
}
req.Canceled = true
pr.sendChannel.Send(req)
}
func (pr *receiver[Payload, Value]) Status() Status[Value] {
return pr.status
}