add internal/{processor,streaming}
This commit is contained in:
parent
6f17b59a47
commit
5c6da51234
6 changed files with 128 additions and 29 deletions
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.sleepycat.moe/sam/mercury/internal/streaming"
|
||||
"git.sleepycat.moe/sam/mercury/web/api"
|
||||
"git.sleepycat.moe/sam/mercury/web/app"
|
||||
"github.com/gorilla/websocket"
|
||||
|
@ -38,7 +39,7 @@ func (app *App) Streaming(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
socket, ok := SocketHolder.socketsFor(token.UserID).newSocket(ctx, cancel)
|
||||
socket, ok := app.Processor.SocketHolder.SocketsFor(token.UserID).NewSocket(ctx, cancel)
|
||||
if !ok {
|
||||
err := conn.WriteJSON(newEvent(EventTypeError, ErrorEvent{Code: api.ErrTooManyStreams, Message: "Too many streams open"}))
|
||||
if err != nil {
|
||||
|
@ -54,43 +55,43 @@ func (app *App) Streaming(w http.ResponseWriter, r *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (app *App) writeStream(conn *websocket.Conn, socket *socket) {
|
||||
func (app *App) writeStream(conn *websocket.Conn, socket *streaming.Socket) {
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-socket.ctx.Done():
|
||||
case <-socket.Done():
|
||||
return
|
||||
case ev := <-socket.ch:
|
||||
case ev := <-socket.Chan():
|
||||
// at this point, the type should already have been filtered, so just send the event
|
||||
err := conn.WriteJSON(ev)
|
||||
if err != nil {
|
||||
// write failed, bail and make client reconnect
|
||||
log.Err(err).Msg("error writing JSON to socket")
|
||||
socket.cancel()
|
||||
socket.Cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) readStream(conn *websocket.Conn, socket *socket) {
|
||||
func (app *App) readStream(conn *websocket.Conn, socket *streaming.Socket) {
|
||||
for {
|
||||
select {
|
||||
case <-socket.ctx.Done():
|
||||
case <-socket.Done():
|
||||
return
|
||||
default:
|
||||
var e IncomingEvent
|
||||
var e streaming.IncomingEvent
|
||||
err := conn.ReadJSON(&e)
|
||||
if err != nil {
|
||||
// read failed, bail and make client reconnect
|
||||
log.Err(err).Msg("error reading JSON from socket")
|
||||
socket.cancel()
|
||||
socket.Cancel()
|
||||
return
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case EventTypeSubscribe, EventTypeUnsubscribe:
|
||||
var et EventType
|
||||
case streaming.EventTypeSubscribe, streaming.EventTypeUnsubscribe:
|
||||
var et streaming.EventType
|
||||
err = json.Unmarshal(e.Data, &et)
|
||||
if err != nil {
|
||||
// invalid event type, log but don't disconnect
|
||||
|
@ -103,7 +104,7 @@ func (app *App) readStream(conn *websocket.Conn, socket *socket) {
|
|||
continue
|
||||
}
|
||||
|
||||
socket.setEvent(et, e.Type != EventTypeSubscribe)
|
||||
socket.SetEvent(et, e.Type != streaming.EventTypeUnsubscribe)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
package streaming
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
var SocketHolder socketHolder
|
||||
|
||||
type socketHolder struct {
|
||||
// map of sockets to
|
||||
sockets map[ulid.ULID]*userSockets
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (sh *socketHolder) Send(acctID ulid.ULID, et EventType, data any) {
|
||||
userSockets := sh.socketsFor(acctID)
|
||||
|
||||
userSockets.mu.Lock()
|
||||
sockets := make([]*socket, len(userSockets.sockets))
|
||||
copy(sockets, userSockets.sockets)
|
||||
userSockets.mu.Unlock()
|
||||
|
||||
for _, s := range sockets {
|
||||
if s.willAcceptEvent(et) {
|
||||
// the socket might block for a bit, so spin this off into a separate goroutine
|
||||
go func(s *socket) {
|
||||
s.ch <- Event{Type: et, Data: data}
|
||||
}(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *socketHolder) socketsFor(acct ulid.ULID) *userSockets {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
us, ok := s.sockets[acct]
|
||||
if !ok {
|
||||
us = &userSockets{}
|
||||
s.sockets[acct] = us
|
||||
}
|
||||
return us
|
||||
}
|
||||
|
||||
const sessionCountLimit = 50 // no more than 50 concurrent sessions per user
|
||||
|
||||
type userSockets struct {
|
||||
mu sync.Mutex
|
||||
sockets []*socket
|
||||
}
|
||||
|
||||
func (s *userSockets) newSocket(ctx context.Context, cancel context.CancelFunc) (*socket, bool) {
|
||||
s.mu.Lock()
|
||||
if len(s.sockets) >= sessionCountLimit {
|
||||
return nil, false
|
||||
}
|
||||
socket := newSocket(ctx, cancel)
|
||||
s.sockets = append(s.sockets, socket)
|
||||
return socket, true
|
||||
}
|
||||
|
||||
type socket struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
ch chan Event
|
||||
types map[EventType]struct{}
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *socket) willAcceptEvent(mt EventType) bool {
|
||||
if mt == EventTypeError {
|
||||
return true
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
_, ok := s.types[mt]
|
||||
s.mu.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *socket) setEvent(mt EventType, add bool) {
|
||||
s.mu.Lock()
|
||||
if add {
|
||||
s.types[mt] = struct{}{}
|
||||
} else {
|
||||
delete(s.types, mt)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func newSocket(ctx context.Context, cancel context.CancelFunc) *socket {
|
||||
return &socket{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
ch: make(chan Event),
|
||||
types: make(map[EventType]struct{}),
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue