Files
poseidon/internal/api/ws/codeocean_writer.go
Maximilian Paß d0dd5c08cb Remove usage of context.DeadlineExceeded
for internal decisions as this error is strongly used by other packages. By checking such wrapped errors the internal decision can be influenced accidentally.
In this case the retry mechanism checked if the error is context.DeadlineExceeded and assumed it would be created by the internal context. This assumption was wrong.
2023-10-31 15:49:56 +01:00

176 lines
6.0 KiB
Go

package ws
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/websocket"
"github.com/openHPI/poseidon/internal/nomad"
"github.com/openHPI/poseidon/internal/runner"
"github.com/openHPI/poseidon/pkg/dto"
"io"
)
// CodeOceanOutputWriterBufferSize defines the number of messages.
const CodeOceanOutputWriterBufferSize = 64
// rawToCodeOceanWriter is a simple io.Writer implementation that just forwards the call to sendMessage.
type rawToCodeOceanWriter struct {
outputType dto.WebSocketMessageType
sendMessage func(*dto.WebSocketMessage)
ctx context.Context
}
// Write implements the io.Writer interface.
func (rc *rawToCodeOceanWriter) Write(p []byte) (int, error) {
switch {
case rc.ctx.Err() != nil:
return 0, fmt.Errorf("CodeOceanWriter context done: %w", rc.ctx.Err())
case len(p) == 0:
return 0, nil
default:
rc.sendMessage(&dto.WebSocketMessage{Type: rc.outputType, Data: string(p)})
return len(p), nil
}
}
// WebSocketWriter is an interface that defines which data is required and which information can be passed.
type WebSocketWriter interface {
StdOut() io.Writer
StdErr() io.Writer
Close(info *runner.ExitInfo)
}
// codeOceanOutputWriter is a concrete WebSocketWriter implementation.
// It forwards the data written to stdOut or stdErr (Nomad, AWS) to the WebSocket connection (CodeOcean).
type codeOceanOutputWriter struct {
connection Connection
stdOut *rawToCodeOceanWriter
stdErr *rawToCodeOceanWriter
queue chan *writingLoopMessage
ctx context.Context
}
// writingLoopMessage is an internal data structure to notify the writing loop when it should stop.
type writingLoopMessage struct {
done bool
data *dto.WebSocketMessage
}
// NewCodeOceanOutputWriter provides an codeOceanOutputWriter for the time the context ctx is active.
// The codeOceanOutputWriter handles all the messages defined in the websocket.schema.json (start, timeout, stdout, ..).
func NewCodeOceanOutputWriter(
connection Connection, ctx context.Context, done context.CancelFunc) WebSocketWriter {
cw := &codeOceanOutputWriter{
connection: connection,
queue: make(chan *writingLoopMessage, CodeOceanOutputWriterBufferSize),
ctx: ctx,
}
cw.stdOut = &rawToCodeOceanWriter{
outputType: dto.WebSocketOutputStdout,
sendMessage: cw.send,
ctx: ctx,
}
cw.stdErr = &rawToCodeOceanWriter{
outputType: dto.WebSocketOutputStderr,
sendMessage: cw.send,
ctx: ctx,
}
go cw.startWritingLoop(done)
cw.send(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart})
return cw
}
// StdOut provides an io.Writer that forwards the written data to CodeOcean as StdOut stream.
func (cw *codeOceanOutputWriter) StdOut() io.Writer {
return cw.stdOut
}
// StdErr provides an io.Writer that forwards the written data to CodeOcean as StdErr stream.
func (cw *codeOceanOutputWriter) StdErr() io.Writer {
return cw.stdErr
}
// Close forwards the kind of exit (timeout, error, normal) to CodeOcean.
// This results in the closing of the WebSocket connection.
// The call of Close is mandatory!
func (cw *codeOceanOutputWriter) Close(info *runner.ExitInfo) {
defer func() { cw.queue <- &writingLoopMessage{done: true} }()
// Mask the internal stop reason before disclosing/forwarding it externally/to CodeOcean.
switch {
case info == nil:
return
case info.Err == nil:
cw.send(&dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: info.Code})
case errors.Is(info.Err, runner.ErrorExecutionTimeout) || errors.Is(info.Err, runner.ErrorRunnerInactivityTimeout):
cw.send(&dto.WebSocketMessage{Type: dto.WebSocketMetaTimeout})
case errors.Is(info.Err, runner.ErrOOMKilled):
cw.send(&dto.WebSocketMessage{Type: dto.WebSocketOutputError, Data: dto.ErrOOMKilled.Error()})
case errors.Is(info.Err, nomad.ErrorAllocationCompleted), errors.Is(info.Err, runner.ErrDestroyedByAPIRequest):
message := "the allocation stopped as expected"
log.WithContext(cw.ctx).WithError(info.Err).Trace(message)
cw.send(&dto.WebSocketMessage{Type: dto.WebSocketOutputError, Data: message})
default:
errorMessage := "Error executing the request"
log.WithContext(cw.ctx).WithError(info.Err).Warn(errorMessage)
cw.send(&dto.WebSocketMessage{Type: dto.WebSocketOutputError, Data: errorMessage})
}
}
// send forwards the passed dto.WebSocketMessage to the writing loop.
func (cw *codeOceanOutputWriter) send(message *dto.WebSocketMessage) {
cw.queue <- &writingLoopMessage{done: false, data: message}
}
// startWritingLoop enables the writing loop.
// This is the central and only place where written changes to the WebSocket connection should be done.
// It synchronizes the messages to provide state checks of the WebSocket connection.
func (cw *codeOceanOutputWriter) startWritingLoop(writingLoopDone context.CancelFunc) {
defer func() {
message := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")
err := cw.connection.WriteMessage(websocket.CloseMessage, message)
err2 := cw.connection.Close()
if err != nil || err2 != nil {
log.WithContext(cw.ctx).WithError(err).WithField("err2", err2).Warn("Error during websocket close")
}
}()
for {
message := <-cw.queue
done := true
if message.data != nil {
done = sendMessage(cw.connection, message.data, cw.ctx)
}
if done || message.done {
log.WithContext(cw.ctx).Trace("Writing loop done")
writingLoopDone()
return
}
}
}
// sendMessage is a helper function for the writing loop. It must not be called from somewhere else!
func sendMessage(connection Connection, message *dto.WebSocketMessage, ctx context.Context) (done bool) {
if message == nil {
return false
}
encodedMessage, err := json.Marshal(message)
if err != nil {
log.WithContext(ctx).WithField("message", message).WithError(err).Warn("Marshal error")
return false
}
log.WithContext(ctx).WithField("message", message).Trace("Sending message to client")
err = connection.WriteMessage(websocket.TextMessage, encodedMessage)
if err != nil {
errorMessage := "Error writing the message"
log.WithContext(ctx).WithField("message", message).WithError(err).Warn(errorMessage)
return true
}
return false
}