github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/pkg/requester/publicapi/endpoints_websockets_events.go (about)

     1  package publicapi
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  
     7  	"github.com/filecoin-project/bacalhau/pkg/model"
     8  	"github.com/gorilla/websocket"
     9  	"github.com/rs/zerolog/log"
    10  )
    11  
    12  var upgrader = websocket.Upgrader{
    13  	ReadBufferSize:  1024,
    14  	WriteBufferSize: 1024,
    15  }
    16  
    17  // TODO: Godoc
    18  func (s *RequesterAPIServer) websocketJobEvents(res http.ResponseWriter, req *http.Request) {
    19  	conn, err := upgrader.Upgrade(res, req, nil)
    20  	if err != nil {
    21  		http.Error(res, err.Error(), http.StatusInternalServerError)
    22  		return
    23  	}
    24  	log.Ctx(req.Context()).Debug().Msgf("New websocketJobEvents connection.")
    25  	defer conn.Close()
    26  
    27  	// NB: jobId == "" is the case for subscriptions to "all events"
    28  
    29  	// get job_id from query string
    30  	jobID := req.URL.Query().Get("job_id")
    31  
    32  	func() {
    33  		s.websocketsMutex.Lock()
    34  		defer s.websocketsMutex.Unlock()
    35  
    36  		sockets, ok := s.websockets[jobID]
    37  		if !ok {
    38  			sockets = []*websocket.Conn{}
    39  			s.websockets[jobID] = sockets
    40  		}
    41  		s.websockets[jobID] = append(sockets, conn)
    42  	}()
    43  
    44  	if jobID != "" {
    45  		// list events for job out of localDB and send them to the client
    46  		events, err := s.jobStore.GetJobHistory(context.Background(), jobID)
    47  		if err != nil {
    48  			log.Ctx(req.Context()).Error().Msgf("error listing job events: %s\n", err.Error())
    49  			return
    50  		}
    51  		for _, event := range events {
    52  			err := conn.WriteJSON(event)
    53  			if err != nil {
    54  				log.Ctx(req.Context()).Error().Msgf("error writing event JSON: %s\n", err.Error())
    55  			}
    56  		}
    57  	}
    58  
    59  	for {
    60  		// read and throw away any incoming messages, exit when client
    61  		// disconnects (which is a sort of error)
    62  		_, _, err := conn.ReadMessage()
    63  		if err != nil {
    64  			break
    65  		}
    66  	}
    67  }
    68  
    69  func (s *RequesterAPIServer) HandleJobEvent(ctx context.Context, event model.JobEvent) (err error) {
    70  	s.websocketsMutex.Lock()
    71  	defer s.websocketsMutex.Unlock()
    72  
    73  	dispatchAndCleanup := func(jobId string) {
    74  		connections, ok := s.websockets[jobId]
    75  		if !ok {
    76  			return
    77  		}
    78  		errIdxs := []int{}
    79  		for idx, connection := range connections {
    80  			// TODO: dispatch to subscribers in parallel, to avoid one slow
    81  			// reader slowing all the others down.
    82  			err := connection.WriteJSON(event)
    83  			if err != nil {
    84  				log.Ctx(ctx).Error().Msgf(
    85  					"error writing event to subscriber '%s'/%d: %s, closing ws\n",
    86  					jobId, idx, err.Error(),
    87  				)
    88  				errIdxs = append(errIdxs, idx)
    89  				// close the connection, if possible, to allow the other side to
    90  				// retry. Ignore errors from closing, since we are going to
    91  				// delete this connection anyway.
    92  				connection.Close()
    93  			}
    94  		}
    95  		// reverse errIdxs (so we don't mess up the indexes for cleanup) and
    96  		// clean up dud connections
    97  		for i := len(errIdxs)/2 - 1; i >= 0; i-- {
    98  			opp := len(errIdxs) - 1 - i
    99  			errIdxs[i], errIdxs[opp] = errIdxs[opp], errIdxs[i]
   100  		}
   101  		for _, idx := range errIdxs {
   102  			connections = append(connections[:idx], connections[idx+1:]...)
   103  		}
   104  		s.websockets[jobId] = connections
   105  	}
   106  	dispatchAndCleanup("")
   107  	dispatchAndCleanup(event.JobID)
   108  	return nil
   109  }