From 77b634e1bd3fa0a98bf86d86925c23380e85ea11 Mon Sep 17 00:00:00 2001 From: lmangani Date: Tue, 8 Oct 2024 21:29:05 +0000 Subject: [PATCH] bootstrap httpserver --- .../workflows/MainDistributionPipeline.yml | 6 +- CMakeLists.txt | 6 +- Makefile | 4 +- extension_config.cmake | 4 +- src/httpserver_extension.cpp | 166 ++++++++++++++++++ src/include/httpserver_extension.hpp | 20 +++ src/include/quack_extension.hpp | 14 -- src/quack_extension.cpp | 78 -------- 8 files changed, 196 insertions(+), 102 deletions(-) create mode 100644 src/httpserver_extension.cpp create mode 100644 src/include/httpserver_extension.hpp delete mode 100644 src/include/quack_extension.hpp delete mode 100644 src/quack_extension.cpp diff --git a/.github/workflows/MainDistributionPipeline.yml b/.github/workflows/MainDistributionPipeline.yml index 50639f9..bfb8cb4 100644 --- a/.github/workflows/MainDistributionPipeline.yml +++ b/.github/workflows/MainDistributionPipeline.yml @@ -18,7 +18,7 @@ jobs: with: duckdb_version: main ci_tools_version: main - extension_name: quack + extension_name: httpserver duckdb-stable-build: name: Build extension binaries @@ -26,7 +26,7 @@ jobs: with: duckdb_version: v1.1.1 ci_tools_version: v1.1.1 - extension_name: quack + extension_name: httpserver duckdb-stable-deploy: name: Deploy extension binaries @@ -35,5 +35,5 @@ jobs: secrets: inherit with: duckdb_version: v1.1.1 - extension_name: quack + extension_name: httpserver deploy_latest: ${{ startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d144aa..8312452 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.5) # Set extension name here -set(TARGET_NAME quack) +set(TARGET_NAME httpserver) # DuckDB's extension distribution supports vcpkg. As such, dependencies can be added in ./vcpkg.json and then # used in cmake with find_package. Feel free to remove or replace with other dependencies. @@ -12,9 +12,9 @@ set(EXTENSION_NAME ${TARGET_NAME}_extension) set(LOADABLE_EXTENSION_NAME ${TARGET_NAME}_loadable_extension) project(${TARGET_NAME}) -include_directories(src/include) +include_directories(src/include duckdb/third_party/httplib) -set(EXTENSION_SOURCES src/quack_extension.cpp) +set(EXTENSION_SOURCES src/httpserver_extension.cpp) build_static_extension(${TARGET_NAME} ${EXTENSION_SOURCES}) build_loadable_extension(${TARGET_NAME} " " ${EXTENSION_SOURCES}) diff --git a/Makefile b/Makefile index e91db43..562b47c 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ PROJ_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) # Configuration of extension -EXT_NAME=quack +EXT_NAME=httpserver EXT_CONFIG=${PROJ_DIR}extension_config.cmake # Include the Makefile from extension-ci-tools -include extension-ci-tools/makefiles/duckdb_extension.Makefile \ No newline at end of file +include extension-ci-tools/makefiles/duckdb_extension.Makefile diff --git a/extension_config.cmake b/extension_config.cmake index 959e702..85a2669 100644 --- a/extension_config.cmake +++ b/extension_config.cmake @@ -1,10 +1,10 @@ # This file is included by DuckDB's build system. It specifies which extension to load # Extension from this repo -duckdb_extension_load(quack +duckdb_extension_load(httpserver SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR} LOAD_TESTS ) # Any extra extensions that should be built -# e.g.: duckdb_extension_load(json) \ No newline at end of file +# e.g.: duckdb_extension_load(json) diff --git a/src/httpserver_extension.cpp b/src/httpserver_extension.cpp new file mode 100644 index 0000000..6f9d745 --- /dev/null +++ b/src/httpserver_extension.cpp @@ -0,0 +1,166 @@ +#define DUCKDB_EXTENSION_MAIN +#include "httpserver_extension.hpp" +#include "duckdb.hpp" +#include "duckdb/common/exception.hpp" +#include "duckdb/common/string_util.hpp" +#include "duckdb/function/scalar_function.hpp" +#include "duckdb/main/extension_util.hpp" +#include "duckdb/common/atomic.hpp" +#include "duckdb/common/exception/http_exception.hpp" + +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.hpp" + +#include +#include + +namespace duckdb { + +struct HttpServerState { + std::unique_ptr server; + std::unique_ptr server_thread; + std::atomic is_running; + DatabaseInstance* db_instance; + + HttpServerState() : is_running(false), db_instance(nullptr) {} +}; + +static HttpServerState global_state; + +static void HandleQuery(const string& query, duckdb_httplib_openssl::Response& res) { + try { + if (!global_state.db_instance) { + throw IOException("Database instance not initialized"); + } + + Connection con(*global_state.db_instance); + auto result = con.Query(query); + + if (result->HasError()) { + res.status = 400; + res.set_content(result->GetError(), "text/plain"); + return; + } + + res.set_content(result->ToString(), "text/plain"); + } catch (const Exception& ex) { + res.status = 400; + res.set_content(ex.what(), "text/plain"); + } +} + +void HttpServerStart(DatabaseInstance& db, string_t host, int32_t port) { + if (global_state.is_running) { + throw IOException("HTTP server is already running"); + } + + global_state.db_instance = &db; + global_state.server.reset(new duckdb_httplib_openssl::Server()); + global_state.is_running = true; + + // Handle GET requests + global_state.server->Get("/query", [](const duckdb_httplib_openssl::Request& req, duckdb_httplib_openssl::Response& res) { + if (!req.has_param("q")) { + res.status = 400; + res.set_content("Missing query parameter 'q'", "text/plain"); + return; + } + + auto query = req.get_param_value("q"); + HandleQuery(query, res); + }); + + // Handle POST requests + global_state.server->Post("/query", [](const duckdb_httplib_openssl::Request& req, duckdb_httplib_openssl::Response& res) { + if (req.body.empty()) { + res.status = 400; + res.set_content("Empty query body", "text/plain"); + return; + } + HandleQuery(req.body, res); + }); + + // Health check endpoint + global_state.server->Get("/health", [](const duckdb_httplib_openssl::Request& req, duckdb_httplib_openssl::Response& res) { + res.set_content("OK", "text/plain"); + }); + + string host_str = host.GetString(); + global_state.server_thread.reset(new std::thread([host_str, port]() { + if (!global_state.server->listen(host_str.c_str(), port)) { + global_state.is_running = false; + throw IOException("Failed to start HTTP server on " + host_str + ":" + std::to_string(port)); + } + })); +} + +void HttpServerStop() { + if (global_state.is_running) { + global_state.server->stop(); + if (global_state.server_thread && global_state.server_thread->joinable()) { + global_state.server_thread->join(); + } + global_state.server.reset(); + global_state.server_thread.reset(); + global_state.db_instance = nullptr; + global_state.is_running = false; + } +} + +static void LoadInternal(DatabaseInstance &instance) { + auto httpserve_start = ScalarFunction("httpserve_start", + {LogicalType::VARCHAR, LogicalType::INTEGER}, + LogicalType::VARCHAR, + [&](DataChunk &args, ExpressionState &state, Vector &result) { + auto &host_vector = args.data[0]; + auto &port_vector = args.data[1]; + + UnaryExecutor::Execute( + host_vector, result, args.size(), + [&](string_t host) { + auto port = ((int32_t*)port_vector.GetData())[0]; + HttpServerStart(instance, host, port); + return StringVector::AddString(result, "HTTP server started on " + host.GetString() + ":" + std::to_string(port)); + }); + }); + + auto httpserve_stop = ScalarFunction("httpserve_stop", + {}, + LogicalType::VARCHAR, + [](DataChunk &args, ExpressionState &state, Vector &result) { + HttpServerStop(); + result.SetValue(0, Value("HTTP server stopped")); + }); + + ExtensionUtil::RegisterFunction(instance, httpserve_start); + ExtensionUtil::RegisterFunction(instance, httpserve_stop); +} + +void HttpserverExtension::Load(DuckDB &db) { + LoadInternal(*db.instance); +} + +std::string HttpserverExtension::Name() { + return "httpserver"; +} + +std::string HttpserverExtension::Version() const { +#ifdef EXT_VERSION_HTTPSERVER + return EXT_VERSION_HTTPSERVER; +#else + return ""; +#endif +} + +} // namespace duckdb + +extern "C" { +DUCKDB_EXTENSION_API void httpserver_init(duckdb::DatabaseInstance &db) { + duckdb::DuckDB db_wrapper(db); + db_wrapper.LoadExtension(); +} + +DUCKDB_EXTENSION_API const char *httpserver_version() { + return duckdb::DuckDB::LibraryVersion(); +} +} diff --git a/src/include/httpserver_extension.hpp b/src/include/httpserver_extension.hpp new file mode 100644 index 0000000..432d1c0 --- /dev/null +++ b/src/include/httpserver_extension.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "duckdb.hpp" +#include "duckdb/common/file_system.hpp" + +namespace duckdb { + +class HttpserverExtension : public Extension { +public: + void Load(DuckDB &db) override; + std::string Name() override; + std::string Version() const override; +}; + +// Static server state declarations +struct HttpServerState; +void HttpServerStart(DatabaseInstance& db, string_t host, int32_t port); +void HttpServerStop(); + +} // namespace duckdb diff --git a/src/include/quack_extension.hpp b/src/include/quack_extension.hpp deleted file mode 100644 index 494467b..0000000 --- a/src/include/quack_extension.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include "duckdb.hpp" - -namespace duckdb { - -class QuackExtension : public Extension { -public: - void Load(DuckDB &db) override; - std::string Name() override; - std::string Version() const override; -}; - -} // namespace duckdb diff --git a/src/quack_extension.cpp b/src/quack_extension.cpp deleted file mode 100644 index 468b2e2..0000000 --- a/src/quack_extension.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#define DUCKDB_EXTENSION_MAIN - -#include "quack_extension.hpp" -#include "duckdb.hpp" -#include "duckdb/common/exception.hpp" -#include "duckdb/common/string_util.hpp" -#include "duckdb/function/scalar_function.hpp" -#include "duckdb/main/extension_util.hpp" -#include - -// OpenSSL linked through vcpkg -#include - -namespace duckdb { - -inline void QuackScalarFun(DataChunk &args, ExpressionState &state, Vector &result) { - auto &name_vector = args.data[0]; - UnaryExecutor::Execute( - name_vector, result, args.size(), - [&](string_t name) { - return StringVector::AddString(result, "Quack "+name.GetString()+" 🐥");; - }); -} - -inline void QuackOpenSSLVersionScalarFun(DataChunk &args, ExpressionState &state, Vector &result) { - auto &name_vector = args.data[0]; - UnaryExecutor::Execute( - name_vector, result, args.size(), - [&](string_t name) { - return StringVector::AddString(result, "Quack " + name.GetString() + - ", my linked OpenSSL version is " + - OPENSSL_VERSION_TEXT );; - }); -} - -static void LoadInternal(DatabaseInstance &instance) { - // Register a scalar function - auto quack_scalar_function = ScalarFunction("quack", {LogicalType::VARCHAR}, LogicalType::VARCHAR, QuackScalarFun); - ExtensionUtil::RegisterFunction(instance, quack_scalar_function); - - // Register another scalar function - auto quack_openssl_version_scalar_function = ScalarFunction("quack_openssl_version", {LogicalType::VARCHAR}, - LogicalType::VARCHAR, QuackOpenSSLVersionScalarFun); - ExtensionUtil::RegisterFunction(instance, quack_openssl_version_scalar_function); -} - -void QuackExtension::Load(DuckDB &db) { - LoadInternal(*db.instance); -} -std::string QuackExtension::Name() { - return "quack"; -} - -std::string QuackExtension::Version() const { -#ifdef EXT_VERSION_QUACK - return EXT_VERSION_QUACK; -#else - return ""; -#endif -} - -} // namespace duckdb - -extern "C" { - -DUCKDB_EXTENSION_API void quack_init(duckdb::DatabaseInstance &db) { - duckdb::DuckDB db_wrapper(db); - db_wrapper.LoadExtension(); -} - -DUCKDB_EXTENSION_API const char *quack_version() { - return duckdb::DuckDB::LibraryVersion(); -} -} - -#ifndef DUCKDB_EXTENSION_MAIN -#error DUCKDB_EXTENSION_MAIN not defined -#endif