Skip to content

Commit

Permalink
Initial BenchKit backend implementation (#566)
Browse files Browse the repository at this point in the history
* added benchkit
  • Loading branch information
StephenCathcart authored Feb 22, 2024
1 parent ed22096 commit ef1b68f
Show file tree
Hide file tree
Showing 5 changed files with 795 additions and 0 deletions.
204 changes: 204 additions & 0 deletions benchkit-backend/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [https://neo4j.com]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package main

import (
"context"
"encoding/json"
"fmt"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
cfg "github.com/neo4j/neo4j-go-driver/v5/neo4j/config"
"github.com/neo4j/neo4j-go-driver/v5/neo4j/log"
"net/http"
"strings"
)

var ctx = context.Background()

type backend struct {
config config
driver neo4j.DriverWithContext
workloads workloads
}

func (b *backend) readyHandler(w http.ResponseWriter, r *http.Request) {
b.ready(w, r)
}

func (b *backend) workloadHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
b.postWorkload(w, r)
case http.MethodPut:
b.putWorkload(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}

func (b *backend) workloadWithIdHandler(w http.ResponseWriter, r *http.Request) {
// Extract the part of the URL path after "/workload/"
workloadId := strings.TrimPrefix(r.URL.Path, "/workload/")
if workloadId == "" {
http.Error(w, "invalid {workloadId}", http.StatusBadRequest)
return
}

// Proceed based on the method
switch r.Method {
case http.MethodGet:
b.getWorkload(w, r, workloadId)
case http.MethodPatch:
b.patchWorkload(w, r, workloadId)
case http.MethodDelete:
b.deleteWorkload(w, r, workloadId)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}

func (b *backend) ready(w http.ResponseWriter, r *http.Request) {
// Create driver and verify connectivity
err := b.createDriver()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Write response
w.WriteHeader(http.StatusOK)
}

func (b *backend) postWorkload(w http.ResponseWriter, r *http.Request) {
var request workload
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}

wl, err := newWorkload(request.Method, request.Queries, request.Database, request.Routing, request.Mode)
if err != nil {
http.Error(w, fmt.Sprintf("failed to create workload: %v", err), http.StatusBadRequest)
return
}

// Store the workload
id := b.workloads.store(wl)

// Set the location header
w.Header().Set("Location", fmt.Sprintf("/workload/%s", id))
w.WriteHeader(http.StatusCreated)
}

func (b *backend) putWorkload(w http.ResponseWriter, r *http.Request) {
var request workload
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}

wl, err := newWorkload(request.Method, request.Queries, request.Database, request.Routing, request.Mode)
if err != nil {
http.Error(w, fmt.Sprintf("failed to create workload: %v", err), http.StatusBadRequest)
return
}

// Execute workload
err = wl.execute(b.driver)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Write response
w.WriteHeader(http.StatusNoContent)
}

func (b *backend) getWorkload(w http.ResponseWriter, r *http.Request, workloadId string) {
// Get workload from our store
workloadFromStore, ok := b.workloads.fetch(workloadId)
if !ok {
http.Error(w, fmt.Sprintf("workload {%s} not found", workloadId), http.StatusNotFound)
return
}

// Execute workload
err := workloadFromStore.execute(b.driver)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Write response
w.WriteHeader(http.StatusNoContent)
}

func (b *backend) patchWorkload(w http.ResponseWriter, r *http.Request, workloadId string) {
var request workload
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}

// Get workload from our store
storedWorkload, ok := b.workloads.fetch(workloadId)
if !ok {
http.Error(w, fmt.Sprintf("workload {%s} not found", workloadId), http.StatusNotFound)
return
}

// Patch the stored workload with the requested workload.
if err := storedWorkload.patch(&request); err != nil {
http.Error(w, fmt.Sprintf("failed to update workload: %v", err), http.StatusBadRequest)
return
}

// Write response
w.WriteHeader(http.StatusOK)
}

func (b *backend) deleteWorkload(w http.ResponseWriter, r *http.Request, workloadId string) {
// Delete workload from our store
ok := b.workloads.delete(workloadId)
if !ok {
http.Error(w, fmt.Sprintf("workload {%s} not found", workloadId), http.StatusNotFound)
return
}

// Write response
w.WriteHeader(http.StatusNoContent)
}

func (b *backend) createDriver() error {
if b.driver == nil {
uri := fmt.Sprintf("%s://%s:%d", b.config.neo4jScheme, b.config.neo4jHost, b.config.neo4jPort)
driver, err := neo4j.NewDriverWithContext(uri, neo4j.BasicAuth(b.config.neo4jUser, b.config.neo4jPass, ""), func(config *cfg.Config) {
if b.config.driverDebug {
config.Log = log.ToConsole(log.DEBUG)
}
})
if err != nil {
return fmt.Errorf("failed to create Neo4j driver: %w", err)
}
if err = driver.VerifyConnectivity(ctx); err != nil {
return fmt.Errorf("failed to connect to Neo4j: %w", err)
}
b.driver = driver
}
return nil
}
78 changes: 78 additions & 0 deletions benchkit-backend/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [https://neo4j.com]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package main

import (
"log"
"os"
"strconv"
)

type config struct {
backendPort int
neo4jHost string
neo4jPort int
neo4jScheme string
neo4jUser string
neo4jPass string
driverDebug bool
}

func makeConfig() config {
return config{
backendPort: getEnv("TEST_BACKEND_PORT", 9000),
neo4jHost: getEnv("TEST_NEO4J_HOST", "localhost"),
neo4jPort: getEnv("TEST_NEO4J_PORT", 7687),
neo4jScheme: getEnv("TEST_NEO4J_SCHEME", "neo4j"),
neo4jUser: getEnv("TEST_NEO4J_USER", "neo4j"),
neo4jPass: getEnv("TEST_NEO4J_PASS", "password"),
driverDebug: getEnv("TEST_DRIVER_DEBUG", false),
}
}

// getEnv is a generic function to get an environment variable and convert it to the type T.
// defaultValue is used if the environment variable is not set or if conversion fails.
func getEnv[T any](key string, defaultValue T) T {
valueStr, exists := os.LookupEnv(key)
if !exists {
return defaultValue
}

var value T
switch any(value).(type) {
case string:
return any(valueStr).(T)
case int:
intValue, err := strconv.Atoi(valueStr)
if err != nil {
log.Printf("Warning: Failed to convert %s to int, using default value. Error: %v", key, err)
return defaultValue
}
return any(intValue).(T)
case bool:
boolValue, err := strconv.ParseBool(valueStr)
if err != nil {
log.Printf("Warning: Failed to convert %s to bool, using default value. Error: %v", key, err)
return defaultValue
}
return any(boolValue).(T)
default:
log.Printf("Warning: Unsupported type for environment variable conversion.")
return defaultValue
}
}
40 changes: 40 additions & 0 deletions benchkit-backend/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [https://neo4j.com]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package main

import (
"fmt"
"log"
"net/http"
)

func main() {
// Create backend and initialize configuration
backend := &backend{config: makeConfig()}

// Define endpoints
http.HandleFunc("/ready", backend.readyHandler)
http.HandleFunc("/workload", backend.workloadHandler)
http.HandleFunc("/workload/", backend.workloadWithIdHandler)

// Start server
log.Printf("Starting server on port %d", backend.config.backendPort)
if err := http.ListenAndServe(fmt.Sprintf(":%d", backend.config.backendPort), nil); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
Loading

0 comments on commit ef1b68f

Please sign in to comment.