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 }