203 lines
5.2 KiB
Go
203 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"numbers/sort"
|
|
"time"
|
|
)
|
|
|
|
// Helper struct for JSON decoding.
|
|
type Numbers struct {
|
|
Numbers []int
|
|
}
|
|
|
|
// The maximum response time of the handlers.
|
|
var MaxResponseTime time.Duration = 495 * time.Millisecond
|
|
|
|
// The main entry point of the backend.
|
|
func main() {
|
|
http.HandleFunc("/numbers", func(w http.ResponseWriter, r *http.Request) {
|
|
numbersHandler(w, r)
|
|
})
|
|
|
|
s := &http.Server{
|
|
Addr: ":8090",
|
|
ReadTimeout: 2 * time.Second,
|
|
WriteTimeout: 500 * time.Millisecond,
|
|
}
|
|
|
|
log.Fatal(s.ListenAndServe())
|
|
}
|
|
|
|
// The main handler. The expected request is of the form:
|
|
// GET /numbers?u=http://example.com/primes&u=http://foobar.com/fibo HTTP/1.0
|
|
// The parameter 'u' will be parsed and all urls will be fetched, which must
|
|
// return a valid JSON that looks like e.g.:
|
|
// { "Numbers": [ 1, 2, 3, 5, 8, 13 ] }
|
|
//
|
|
// Then these lists are merged, deduplicated and sorted and the response will
|
|
// be a JSON of the same form.
|
|
// The handler is guaranteed to respond within a timeframe of 500ms. If
|
|
// URLs take too long to load or return garbage, they are skipped.
|
|
// If all URLs take too long to load or return garbage, an empty JSON list
|
|
// is returned.
|
|
func numbersHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), MaxResponseTime)
|
|
defer cancel()
|
|
|
|
var rurl []string = r.URL.Query()["u"]
|
|
// if no parameters, return 400
|
|
if len(rurl) == 0 {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte("Bad request: Missing 'u' parameters"))
|
|
return
|
|
}
|
|
|
|
// Non-blocking channel for sorted results
|
|
sortChan := make(chan []int, len(rurl))
|
|
|
|
// fetch all URLs asynchronously and sort them
|
|
for i := range rurl {
|
|
go func(url string) {
|
|
n, e := getNumbers(url, ctx)
|
|
|
|
if e == nil { // we have a usable JSON list of numbers
|
|
if n != nil && len(n) > 0 { // the list of numbers is non-empty
|
|
sorted, err := sort.SortedAndDedup(ctx, n)
|
|
if err != nil { // sorting threw an error
|
|
log.Printf("Sorting took too long, ignoring list")
|
|
return
|
|
}
|
|
sortChan <- sorted
|
|
} else { // empty list of numbers
|
|
log.Printf("Received empty list of numbers from endpoint")
|
|
}
|
|
} else { // no usable JSON/other error
|
|
log.Printf("Got an error: %s", e)
|
|
}
|
|
}(rurl[i])
|
|
}
|
|
|
|
// result list that is returned on short-circuit
|
|
var resultList []int = []int{}
|
|
|
|
// counter of how many merge operations have been performed
|
|
mc := 0
|
|
|
|
// get all sorted lists concurrently and merge them as we go
|
|
Outer:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Printf("Waiting for URL took too long, finishing response anyway")
|
|
finishResponse(w, resultList)
|
|
return
|
|
case res := <-sortChan:
|
|
merged := sort.MergeLists(resultList, res)
|
|
resultList = merged
|
|
mc += 1
|
|
// if we have len(rurl) merge operations, then
|
|
// all lists have been processed already
|
|
if mc >= len(rurl) {
|
|
break Outer
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("Result is complete, finishing response")
|
|
finishResponse(w, resultList)
|
|
}
|
|
|
|
// Finalizes the JSON response with the given numbers. This always
|
|
// sends a 200 HTTP status code.
|
|
func finishResponse(w http.ResponseWriter, numbers []int) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"Numbers": numbers})
|
|
}
|
|
|
|
// Gets the numbers from the given url.
|
|
// 'resp' is always nil if there was an error. Errors can
|
|
// be url parse errors, HTTP response errors, io errors from reading the
|
|
// body or json decoding errors.
|
|
func getNumbers(rawurl string, ctx context.Context) (resp []int, err error) {
|
|
// validate url
|
|
u_err := validateURL(rawurl)
|
|
if u_err != nil {
|
|
return nil, u_err
|
|
}
|
|
|
|
// retrieve response
|
|
client := &http.Client{}
|
|
req, r_err := http.NewRequest("GET", rawurl, nil)
|
|
if r_err != nil {
|
|
return nil, r_err
|
|
}
|
|
req = req.WithContext(ctx)
|
|
r, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("HTTP: Status code is not 200, but %d",
|
|
r.StatusCode)
|
|
}
|
|
|
|
// open body
|
|
defer r.Body.Close()
|
|
body, b_err := ioutil.ReadAll(r.Body)
|
|
if b_err != nil {
|
|
return nil, b_err
|
|
}
|
|
|
|
// parse json
|
|
return parseJson(body)
|
|
}
|
|
|
|
// Parse the given raw JSON bytes into a list of numbers. The JSON
|
|
// is expected to be of the form:
|
|
// {"Numbers": [1,2,5]}
|
|
//
|
|
// Not particularly strict.
|
|
func parseJson(body []byte) (res []int, err error) {
|
|
dec := json.NewDecoder(bytes.NewReader(body))
|
|
var n Numbers
|
|
j_err := dec.Decode(&n)
|
|
if j_err != nil {
|
|
return nil, j_err
|
|
} else {
|
|
if n.Numbers == nil {
|
|
return nil, fmt.Errorf("JSON: missing key 'Numbers'")
|
|
}
|
|
return n.Numbers, nil
|
|
}
|
|
}
|
|
|
|
// Validate the URL. The URL has to be syntactically valid,
|
|
// has to have a scheme of either 'https' or 'http' and a hostname.
|
|
// An error is returned on invalid URLs.
|
|
func validateURL(rawurl string) error {
|
|
u, u_err := url.Parse(rawurl)
|
|
if u_err != nil {
|
|
return u_err
|
|
}
|
|
if u.Scheme != "https" && u.Scheme != "http" {
|
|
return fmt.Errorf("URL: not a valid HTTP/HTTPS scheme in %s", rawurl)
|
|
}
|
|
if u.Host == "" {
|
|
return fmt.Errorf("URL: not a valid host in %s", rawurl)
|
|
}
|
|
|
|
return nil
|
|
}
|