From 5c7b8038a100f77802712b6d983a1f3097154b4d Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 6 Nov 2025 13:49:02 -0600 Subject: [PATCH] web: Initial backend implementation --- web/backend/.gitignore | 1 + web/backend/.keep | 0 web/backend/Project.toml | 17 +++++++ web/backend/src/Backend.jl | 67 ++++++++++++++++++++++++ web/backend/test/Project.toml | 23 +++++++++ web/backend/test/src/BackendT.jl | 87 ++++++++++++++++++++++++++++++++ 6 files changed, 195 insertions(+) create mode 100644 web/backend/.gitignore delete mode 100644 web/backend/.keep create mode 100644 web/backend/Project.toml create mode 100644 web/backend/src/Backend.jl create mode 100644 web/backend/test/Project.toml create mode 100644 web/backend/test/src/BackendT.jl diff --git a/web/backend/.gitignore b/web/backend/.gitignore new file mode 100644 index 0000000..995321f --- /dev/null +++ b/web/backend/.gitignore @@ -0,0 +1 @@ +jobs diff --git a/web/backend/.keep b/web/backend/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/web/backend/Project.toml b/web/backend/Project.toml new file mode 100644 index 0000000..00b09c8 --- /dev/null +++ b/web/backend/Project.toml @@ -0,0 +1,17 @@ +name = "Backend" +uuid = "948642ed-e3f9-4642-9296-0f1eaf40c938" +version = "0.1.0" +authors = ["Alinson S. Xavier "] + +[deps] +CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193" +HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877" + +[compat] +CodecZlib = "0.7.8" +HTTP = "1.10.19" +JSON = "0.21.4" +Random = "1.11.0" diff --git a/web/backend/src/Backend.jl b/web/backend/src/Backend.jl new file mode 100644 index 0000000..548a911 --- /dev/null +++ b/web/backend/src/Backend.jl @@ -0,0 +1,67 @@ +# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment +# Copyright (C) 2025, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +module Backend + +using HTTP +using Random +using JSON +using CodecZlib +using UnitCommitment + +basedir = joinpath(dirname(@__FILE__), "..") + +function submit(req; optimizer) + # Check if request body is empty + compressed_body = HTTP.payload(req) + if isempty(compressed_body) + return HTTP.Response(400, "Error: No file provided") + end + + # Validate compressed JSON by decompressing and parsing + try + decompressed_data = transcode(GzipDecompressor, compressed_body) + JSON.parse(String(decompressed_data)) + catch e + return HTTP.Response(400, "Error: Invalid compressed JSON") + end + + # Generate random job ID (lowercase letters and numbers) + job_id = randstring(['a':'z'; '0':'9'], 16) + + # Create job directory + job_dir = joinpath(basedir, "jobs", job_id) + mkpath(job_dir) + + # Save input file + json_path = joinpath(job_dir, "input.json.gz") + write(json_path, compressed_body) + + # Optimize file + instance = UnitCommitment.read(json_path) + model = UnitCommitment.build_model(; instance, optimizer) + UnitCommitment.optimize!(model) + solution = UnitCommitment.solution(model) + UnitCommitment.write("$job_dir/output.json", solution) + + # Return job ID as JSON + response_body = JSON.json(Dict("job_id" => job_id)) + return HTTP.Response(200, response_body) +end + +function jobs_view(req) + return HTTP.Response(200, "OK") +end + + +function start_server(port::Int = 8080; optimizer) + Random.seed!() + router = HTTP.Router() + HTTP.register!(router, "POST", "/submit", req -> submit(req; optimizer)) + HTTP.register!(router, "GET", "/jobs/*/view", jobs_view) + server = HTTP.serve!(router, port; verbose = false) + return server +end + +end diff --git a/web/backend/test/Project.toml b/web/backend/test/Project.toml new file mode 100644 index 0000000..c1c0ddf --- /dev/null +++ b/web/backend/test/Project.toml @@ -0,0 +1,23 @@ +name = "BackendT" +uuid = "27da795e-16fd-43bd-a2ba-f77bdecaf977" +version = "0.1.0" +authors = ["Alinson S. Xavier "] + +[deps] +Backend = "948642ed-e3f9-4642-9296-0f1eaf40c938" +CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193" +HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" +HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +CodecZlib = "0.7.8" +HTTP = "1.10.19" +HiGHS = "1.20.1" +JSON = "0.21.4" +JuliaFormatter = "2.2.0" +Revise = "3.12.0" +Test = "1.11.0" diff --git a/web/backend/test/src/BackendT.jl b/web/backend/test/src/BackendT.jl new file mode 100644 index 0000000..3f6e3bb --- /dev/null +++ b/web/backend/test/src/BackendT.jl @@ -0,0 +1,87 @@ +# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment +# Copyright (C) 2025, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +module BackendT + +using Test +using HTTP +using JSON +using CodecZlib +import Backend +import JuliaFormatter +using HiGHS + +basedir = dirname(@__FILE__) +port = 32617 + +function fixture(path::String)::String + return "$basedir/../fixtures/$path" +end + +function with_server(f) + logger = Test.TestLogger() + # server = Base.CoreLogging.with_logger(logger) do + # Backend.start_server(port) + # end + server = Backend.start_server(port; optimizer=HiGHS.Optimizer) + try + f() + finally + close(server) + end + return filter!(x -> x.group == :access, logger.logs) +end + +function test_usage() + with_server() do + # Read the compressed fixture file + compressed_data = read(fixture("case14.json.gz")) + + # Submit test case + response = HTTP.post( + "http://localhost:$port/submit", + ["Content-Type" => "application/gzip"], + compressed_data + ) + @test response.status == 200 + + # Check response + response_data = JSON.parse(String(response.body)) + @test haskey(response_data, "job_id") + job_id = response_data["job_id"] + @test length(job_id) == 16 + @test all(c -> c in ['a':'z'; '0':'9'], collect(job_id)) + + # Verify the compressed file was saved correctly + job_dir = joinpath(Backend.basedir, "jobs", job_id) + saved_path = joinpath(job_dir, "input.json.gz") + @test isfile(saved_path) + saved_data = read(saved_path) + @test saved_data == compressed_data + + response = HTTP.get("http://localhost:$port/jobs/123/view") + @test response.status == 200 + @test String(response.body) == "OK" + + # Clean up: remove the job directory + # rm(job_dir, recursive=true) + end +end + +function runtests() + @testset "UCJL Backend" begin + test_usage() + end + return +end + +function format() + JuliaFormatter.format(basedir, verbose = true) + JuliaFormatter.format("$basedir/../../src", verbose = true) + return +end + +export runtests, format + +end