Add database custom interface functions

This commit is contained in:
Vladimir Stoilov 2023-06-02 11:41:38 +03:00 committed by Daniel
parent e033cff403
commit df62abdf1b
5 changed files with 166 additions and 128 deletions

View file

@ -44,7 +44,7 @@ var (
func init() { func init() {
RegisterHandler("/api/database/v1", WrapInAuthHandler( RegisterHandler("/api/database/v1", WrapInAuthHandler(
startDatabaseAPI, startDatabaseWebsocketAPI,
// Default to admin read/write permissions until the database gets support // Default to admin read/write permissions until the database gets support
// for api permissions. // for api permissions.
dbCompatibilityPermission, dbCompatibilityPermission,
@ -54,9 +54,6 @@ func init() {
// DatabaseAPI is a database API instance. // DatabaseAPI is a database API instance.
type DatabaseAPI struct { type DatabaseAPI struct {
conn *websocket.Conn
sendQueue chan []byte
queriesLock sync.Mutex queriesLock sync.Mutex
queries map[string]*iterator.Iterator queries map[string]*iterator.Iterator
@ -66,13 +63,33 @@ type DatabaseAPI struct {
shutdownSignal chan struct{} shutdownSignal chan struct{}
shuttingDown *abool.AtomicBool shuttingDown *abool.AtomicBool
db *database.Interface db *database.Interface
sendBytes func(data []byte)
}
type DatabaseWebsocketAPI struct {
DatabaseAPI
sendQueue chan []byte
conn *websocket.Conn
} }
func allowAnyOrigin(r *http.Request) bool { func allowAnyOrigin(r *http.Request) bool {
return true return true
} }
func startDatabaseAPI(w http.ResponseWriter, r *http.Request) { func CreateDatabaseAPI(sendFunction func(data []byte)) DatabaseAPI {
return DatabaseAPI{
queries: make(map[string]*iterator.Iterator),
subs: make(map[string]*database.Subscription),
shutdownSignal: make(chan struct{}),
shuttingDown: abool.NewBool(false),
db: database.NewInterface(nil),
sendBytes: sendFunction,
}
}
func startDatabaseWebsocketAPI(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{ upgrader := websocket.Upgrader{
CheckOrigin: allowAnyOrigin, CheckOrigin: allowAnyOrigin,
ReadBufferSize: 1024, ReadBufferSize: 1024,
@ -86,14 +103,21 @@ func startDatabaseAPI(w http.ResponseWriter, r *http.Request) {
return return
} }
newDBAPI := &DatabaseAPI{ newDBAPI := &DatabaseWebsocketAPI{
conn: wsConn, DatabaseAPI: DatabaseAPI{
sendQueue: make(chan []byte, 100), queries: make(map[string]*iterator.Iterator),
queries: make(map[string]*iterator.Iterator), subs: make(map[string]*database.Subscription),
subs: make(map[string]*database.Subscription), shutdownSignal: make(chan struct{}),
shutdownSignal: make(chan struct{}), shuttingDown: abool.NewBool(false),
shuttingDown: abool.NewBool(false), db: database.NewInterface(nil),
db: database.NewInterface(nil), },
sendQueue: make(chan []byte, 100),
conn: wsConn,
}
newDBAPI.sendBytes = func(data []byte) {
newDBAPI.sendQueue <- data
} }
module.StartWorker("database api handler", newDBAPI.handler) module.StartWorker("database api handler", newDBAPI.handler)
@ -102,11 +126,76 @@ func startDatabaseAPI(w http.ResponseWriter, r *http.Request) {
log.Tracer(r.Context()).Infof("api request: init websocket %s %s", r.RemoteAddr, r.RequestURI) log.Tracer(r.Context()).Infof("api request: init websocket %s %s", r.RemoteAddr, r.RequestURI)
} }
func (api *DatabaseAPI) handler(context.Context) error { func (api *DatabaseWebsocketAPI) handler(context.Context) error {
defer func() { defer func() {
_ = api.shutdown(nil) _ = api.shutdown(nil)
}() }()
for {
_, msg, err := api.conn.ReadMessage()
if err != nil {
return api.shutdown(err)
}
api.Handle(msg)
}
}
func (api *DatabaseWebsocketAPI) writer(ctx context.Context) error {
defer func() {
_ = api.shutdown(nil)
}()
var data []byte
var err error
for {
select {
// prioritize direct writes
case data = <-api.sendQueue:
if len(data) == 0 {
return nil
}
case <-ctx.Done():
return nil
case <-api.shutdownSignal:
return nil
}
// log.Tracef("api: sending %s", string(*msg))
err = api.conn.WriteMessage(websocket.BinaryMessage, data)
if err != nil {
return api.shutdown(err)
}
}
}
func (api *DatabaseWebsocketAPI) shutdown(err error) error {
// Check if we are the first to shut down.
if !api.shuttingDown.SetToIf(false, true) {
return nil
}
// Check the given error.
if err != nil {
if websocket.IsCloseError(err,
websocket.CloseNormalClosure,
websocket.CloseGoingAway,
websocket.CloseAbnormalClosure,
) {
log.Infof("api: websocket connection to %s closed", api.conn.RemoteAddr())
} else {
log.Warningf("api: websocket connection error with %s: %s", api.conn.RemoteAddr(), err)
}
}
// Trigger shutdown.
close(api.shutdownSignal)
_ = api.conn.Close()
return nil
}
func (api *DatabaseAPI) Handle(msg []byte) {
// 123|get|<key> // 123|get|<key>
// 123|ok|<key>|<data> // 123|ok|<key>|<data>
// 123|error|<message> // 123|error|<message>
@ -145,124 +234,62 @@ func (api *DatabaseAPI) handler(context.Context) error {
// 131|success // 131|success
// 131|error|<message> // 131|error|<message>
for { parts := bytes.SplitN(msg, []byte("|"), 3)
_, msg, err := api.conn.ReadMessage() // Handle special command "cancel"
if err != nil { if len(parts) == 2 && string(parts[1]) == "cancel" {
return api.shutdown(err) // 124|cancel
} // 125|cancel
// 127|cancel
go api.handleCancel(parts[0])
return
}
parts := bytes.SplitN(msg, []byte("|"), 3) if len(parts) != 3 {
api.send(nil, dbMsgTypeError, "bad request: malformed message", nil)
return
}
// Handle special command "cancel" switch string(parts[1]) {
if len(parts) == 2 && string(parts[1]) == "cancel" { case "get":
// 124|cancel // 123|get|<key>
// 125|cancel go api.handleGet(parts[0], string(parts[2]))
// 127|cancel case "query":
go api.handleCancel(parts[0]) // 124|query|<query>
continue go api.handleQuery(parts[0], string(parts[2]))
} case "sub":
// 125|sub|<query>
if len(parts) != 3 { go api.handleSub(parts[0], string(parts[2]))
case "qsub":
// 127|qsub|<query>
go api.handleQsub(parts[0], string(parts[2]))
case "create", "update", "insert":
// split key and payload
dataParts := bytes.SplitN(parts[2], []byte("|"), 2)
if len(dataParts) != 2 {
api.send(nil, dbMsgTypeError, "bad request: malformed message", nil) api.send(nil, dbMsgTypeError, "bad request: malformed message", nil)
continue return
} }
switch string(parts[1]) { switch string(parts[1]) {
case "get": case "create":
// 123|get|<key> // 128|create|<key>|<data>
go api.handleGet(parts[0], string(parts[2])) go api.handlePut(parts[0], string(dataParts[0]), dataParts[1], true)
case "query": case "update":
// 124|query|<query> // 129|update|<key>|<data>
go api.handleQuery(parts[0], string(parts[2])) go api.handlePut(parts[0], string(dataParts[0]), dataParts[1], false)
case "sub": case "insert":
// 125|sub|<query> // 130|insert|<key>|<data>
go api.handleSub(parts[0], string(parts[2])) go api.handleInsert(parts[0], string(dataParts[0]), dataParts[1])
case "qsub":
// 127|qsub|<query>
go api.handleQsub(parts[0], string(parts[2]))
case "create", "update", "insert":
// split key and payload
dataParts := bytes.SplitN(parts[2], []byte("|"), 2)
if len(dataParts) != 2 {
api.send(nil, dbMsgTypeError, "bad request: malformed message", nil)
continue
}
switch string(parts[1]) {
case "create":
// 128|create|<key>|<data>
go api.handlePut(parts[0], string(dataParts[0]), dataParts[1], true)
case "update":
// 129|update|<key>|<data>
go api.handlePut(parts[0], string(dataParts[0]), dataParts[1], false)
case "insert":
// 130|insert|<key>|<data>
go api.handleInsert(parts[0], string(dataParts[0]), dataParts[1])
}
case "delete":
// 131|delete|<key>
go api.handleDelete(parts[0], string(parts[2]))
default:
api.send(parts[0], dbMsgTypeError, "bad request: unknown method", nil)
} }
case "delete":
// 131|delete|<key>
go api.handleDelete(parts[0], string(parts[2]))
default:
api.send(parts[0], dbMsgTypeError, "bad request: unknown method", nil)
} }
} }
func (api *DatabaseAPI) writer(ctx context.Context) error {
defer func() {
_ = api.shutdown(nil)
}()
var data []byte
var err error
for {
select {
// prioritize direct writes
case data = <-api.sendQueue:
if len(data) == 0 {
return nil
}
case <-ctx.Done():
return nil
case <-api.shutdownSignal:
return nil
}
// log.Tracef("api: sending %s", string(*msg))
err = api.conn.WriteMessage(websocket.BinaryMessage, data)
if err != nil {
return api.shutdown(err)
}
}
}
func (api *DatabaseAPI) shutdown(err error) error {
// Check if we are the first to shut down.
if !api.shuttingDown.SetToIf(false, true) {
return nil
}
// Check the given error.
if err != nil {
if websocket.IsCloseError(err,
websocket.CloseNormalClosure,
websocket.CloseGoingAway,
websocket.CloseAbnormalClosure,
) {
log.Infof("api: websocket connection to %s closed", api.conn.RemoteAddr())
} else {
log.Warningf("api: websocket connection error with %s: %s", api.conn.RemoteAddr(), err)
}
}
// Trigger shutdown.
close(api.shutdownSignal)
_ = api.conn.Close()
return nil
}
func (api *DatabaseAPI) send(opID []byte, msgType string, msgOrKey string, data []byte) { func (api *DatabaseAPI) send(opID []byte, msgType string, msgOrKey string, data []byte) {
c := container.New(opID) c := container.New(opID)
c.Append(dbAPISeperatorBytes) c.Append(dbAPISeperatorBytes)
@ -278,7 +305,7 @@ func (api *DatabaseAPI) send(opID []byte, msgType string, msgOrKey string, data
c.Append(data) c.Append(data)
} }
api.sendQueue <- c.CompileData() api.sendBytes(c.CompileData())
} }
func (api *DatabaseAPI) handleGet(opID []byte, key string) { func (api *DatabaseAPI) handleGet(opID []byte, key string) {
@ -367,7 +394,7 @@ func (api *DatabaseAPI) processQuery(opID []byte, q *query.Query) (ok bool) {
} }
} }
// func (api *DatabaseAPI) runQuery() // func (api *DatabaseWebsocketAPI) runQuery()
func (api *DatabaseAPI) handleSub(opID []byte, queryText string) { func (api *DatabaseAPI) handleSub(opID []byte, queryText string) {
// 125|sub|<query> // 125|sub|<query>

View file

@ -224,6 +224,17 @@ func RegisterEndpoint(e Endpoint) error {
return nil return nil
} }
func GetEndpointByPath(path string) (*Endpoint, error) {
endpointsLock.Lock()
defer endpointsLock.Unlock()
endpoint, ok := endpoints[path]
if !ok {
return nil, fmt.Errorf("no registered endpoint on path: %q", path)
}
return endpoint, nil
}
func (e *Endpoint) check() error { func (e *Endpoint) check() error {
// Check path. // Check path.
if strings.TrimSpace(e.Path) == "" { if strings.TrimSpace(e.Path) == "" {

View file

@ -33,11 +33,11 @@ type Request struct {
// apiRequestContextKey is a key used for the context key/value storage. // apiRequestContextKey is a key used for the context key/value storage.
type apiRequestContextKey struct{} type apiRequestContextKey struct{}
var requestContextKey = apiRequestContextKey{} var RequestContextKey = apiRequestContextKey{}
// GetAPIRequest returns the API Request of the given http request. // GetAPIRequest returns the API Request of the given http request.
func GetAPIRequest(r *http.Request) *Request { func GetAPIRequest(r *http.Request) *Request {
ar, ok := r.Context().Value(requestContextKey).(*Request) ar, ok := r.Context().Value(RequestContextKey).(*Request)
if ok { if ok {
return ar return ar
} }

View file

@ -118,7 +118,7 @@ func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
apiRequest := &Request{ apiRequest := &Request{
Request: r, Request: r,
} }
ctx = context.WithValue(ctx, requestContextKey, apiRequest) ctx = context.WithValue(ctx, RequestContextKey, apiRequest)
// Add context back to request. // Add context back to request.
r = r.WithContext(ctx) r = r.WithContext(ctx)
lrw := NewLoggingResponseWriter(w, r) lrw := NewLoggingResponseWriter(w, r)

View file

@ -184,7 +184,7 @@ func Errorf(format string, things ...interface{}) {
} }
} }
// Critical is used to log events that completely break the system. Operation connot continue. User/Admin must be informed. // Critical is used to log events that completely break the system. Operation cannot continue. User/Admin must be informed.
func Critical(msg string) { func Critical(msg string) {
atomic.AddUint64(critLogLines, 1) atomic.AddUint64(critLogLines, 1)
if fastcheck(CriticalLevel) { if fastcheck(CriticalLevel) {
@ -192,7 +192,7 @@ func Critical(msg string) {
} }
} }
// Criticalf is used to log events that completely break the system. Operation connot continue. User/Admin must be informed. // Criticalf is used to log events that completely break the system. Operation cannot continue. User/Admin must be informed.
func Criticalf(format string, things ...interface{}) { func Criticalf(format string, things ...interface{}) {
atomic.AddUint64(critLogLines, 1) atomic.AddUint64(critLogLines, 1)
if fastcheck(CriticalLevel) { if fastcheck(CriticalLevel) {