From d67596acd921f75caaa4e604bc1ccf163bd0729e Mon Sep 17 00:00:00 2001 From: JP Appel Date: Wed, 2 Jul 2025 02:11:26 -0400 Subject: Add experimental http server --- cmd/atlas.go | 20 +++++++++++----- cmd/index.go | 2 +- cmd/query.go | 2 +- cmd/server.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ pkg/query/query.go | 20 +++++++++++++++- pkg/server/server.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 cmd/server.go create mode 100644 pkg/server/server.go diff --git a/cmd/atlas.go b/cmd/atlas.go index a641521..4474c7e 100644 --- a/cmd/atlas.go +++ b/cmd/atlas.go @@ -44,10 +44,11 @@ func printHelp() { fmt.Println("atlas is a note indexing and querying tool") fmt.Printf("\nUsage:\n %s [global-flags] \n\n", os.Args[0]) fmt.Println("Commands:") - fmt.Println(" index - build, update, or modify the index") - fmt.Println(" query - search against the index") - fmt.Println(" shell - start a debug query shell") - fmt.Println(" help - print this help then exit") + fmt.Println(" index - build, update, or modify the index") + fmt.Println(" query - search against the index") + fmt.Println(" shell - start a debug shell") + fmt.Println(" server - start an http query server (EXPERIMENTAL)") + fmt.Println(" help - print this help then exit") } func main() { @@ -72,16 +73,19 @@ func main() { indexFs := flag.NewFlagSet("index flags", flag.ExitOnError) queryFs := flag.NewFlagSet("query flags", flag.ExitOnError) shellFs := flag.NewFlagSet("debug shell flags", flag.ExitOnError) + serverFs := flag.NewFlagSet("server flags", flag.ExitOnError) indexFs.Usage = addGlobalFlagUsage(indexFs) queryFs.Usage = addGlobalFlagUsage(queryFs) shellFs.Usage = addGlobalFlagUsage(shellFs) + serverFs.Usage = addGlobalFlagUsage(serverFs) flag.Parse() args := flag.Args() queryFlags := QueryFlags{Outputer: query.DefaultOutput{}} indexFlags := IndexFlags{} + serverFlags := ServerFlags{Port: 8080} if len(args) < 1 { fmt.Fprintln(os.Stderr, "No Command provided") @@ -94,9 +98,11 @@ func main() { switch command { case "query", "q": - setupQueryFlags(args, queryFs, &queryFlags) + setupQueryFlags(args[1:], queryFs, &queryFlags) case "index": - setupIndexFlags(args, indexFs, &indexFlags) + setupIndexFlags(args[1:], indexFs, &indexFlags) + case "server": + setupServerFlags(args[1:], serverFs, &serverFlags) case "help": printHelp() flag.PrintDefaults() @@ -151,6 +157,8 @@ func main() { exitCode = int(runQuery(globalFlags, queryFlags, querier, searchQuery)) case "index": exitCode = int(runIndex(globalFlags, indexFlags, querier)) + case "server": + exitCode = int(runServer(globalFlags, serverFlags, querier)) case "shell": state := make(shell.State) env := make(map[string]string) diff --git a/cmd/index.go b/cmd/index.go index c5d1107..62b9af0 100644 --- a/cmd/index.go +++ b/cmd/index.go @@ -43,7 +43,7 @@ func setupIndexFlags(args []string, fs *flag.FlagSet, flags *IndexFlags) { return nil }) - fs.Parse(args[1:]) + fs.Parse(args) } func runIndex(gFlags GlobalFlags, iFlags IndexFlags, db *data.Query) byte { diff --git a/cmd/query.go b/cmd/query.go index 6aa5ca2..9649b34 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -40,7 +40,7 @@ func setupQueryFlags(args []string, fs *flag.FlagSet, flags *QueryFlags) { fs.StringVar(&flags.CustomFormat, "outCustomFormat", query.DefaultOutputFormat, "format string for --outFormat custom, see EXAMPLES for more details") fs.IntVar(&flags.OptimizationLevel, "optLevel", 0, "optimization `level` for queries, 0 is automatic, <0 to disable") - fs.Parse(args[1:]) + fs.Parse(args) } func runQuery(gFlags GlobalFlags, qFlags QueryFlags, db *data.Query, searchQuery string) byte { diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..374e5f4 --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/jpappel/atlas/pkg/data" + "github.com/jpappel/atlas/pkg/server" +) + +type ServerFlags struct { + Address string + Port int +} + +func setupServerFlags(args []string, fs *flag.FlagSet, flags *ServerFlags) { + fs.StringVar(&flags.Address, "address", "", "the address to listen on") + fs.IntVar(&flags.Port, "port", 8080, "the port to bind to") + + fs.Parse(args) +} + +func runServer(gFlags GlobalFlags, sFlags ServerFlags, db *data.Query) byte { + addr := fmt.Sprintf("%s:%d", sFlags.Address, sFlags.Port) + + s := http.Server{Addr: addr, Handler: server.New(db)} + + serverErrors := make(chan error, 1) + exit := make(chan os.Signal, 1) + + signal.Notify(exit, syscall.SIGTERM, os.Interrupt) + + slog.Info("Starting server on", slog.String("addr", addr)) + go func(serverErrors chan<- error) { + if err := s.ListenAndServe(); err != nil { + serverErrors <- err + } + }(serverErrors) + + select { + case <-exit: + slog.Info("Recieved signal to shutdown") + case err := <-serverErrors: + slog.Error("Server error", err) + } + + slog.Info("Shutting down server") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := s.Shutdown(ctx); err != nil { + slog.Error("Error shutting down server", err) + return 1 + } + + return 0 +} diff --git a/pkg/query/query.go b/pkg/query/query.go index 57ba3e1..3552c3f 100644 --- a/pkg/query/query.go +++ b/pkg/query/query.go @@ -1,9 +1,27 @@ package query -import "strings" +import ( + "fmt" + "strings" +) func writeIndent(b *strings.Builder, level int) { for range level { b.WriteByte('\t') } } + +func Compile(userQuery string, optimizationLevel int, numWorkers uint) (CompilationArtifact, error) { + if numWorkers == 0 { + return CompilationArtifact{}, fmt.Errorf("Cannot compile with 0 workers") + } + + clause, err := Parse(Lex(userQuery)) + if err != nil { + return CompilationArtifact{}, err + } + + NewOptimizer(clause, numWorkers).Optimize(optimizationLevel) + + return clause.Compile() +} diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000..8b66e62 --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,67 @@ +package server + +import ( + "bytes" + "io" + "log/slog" + "net/http" + "strings" + + "github.com/jpappel/atlas/pkg/data" + "github.com/jpappel/atlas/pkg/index" + "github.com/jpappel/atlas/pkg/query" +) + +func info(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(` +

Atlas Server

+

This is the experimental atlas server! + Try POSTing a query to

/search

+ `)) +} + +func New(db *data.Query) *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/", info) + mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) { + b := &strings.Builder{} + if _, err := io.Copy(b, r.Body); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Error processing request")) + slog.Error("Error reading request body", slog.String("err", err.Error())) + return + } + artifact, err := query.Compile(b.String(), 0, 1) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + slog.Error("Error compiling query", slog.String("err", err.Error())) + return + } + + pathDocs, err := db.Execute(artifact) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Error executing query")) + slog.Error("Error executing query", slog.String("err", err.Error())) + return + } + docs := make([]*index.Document, 0, len(pathDocs)) + for _, doc := range pathDocs { + docs = append(docs, doc) + } + + var buf bytes.Buffer + _, err = query.JsonOutput{}.OutputTo(&buf, docs) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Error while writing output")) + slog.Error("Error writing json output", slog.String("err", err.Error())) + } + + io.Copy(w, &buf) + }) + + return mux +} -- cgit v1.2.3