232 lines
5.8 KiB
Go
232 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"numbers/sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Helper struct for JSON decoding.
|
|
type Numbers struct {
|
|
Numbers []int
|
|
}
|
|
|
|
// The maximum response time of the handlers.
|
|
var MaxResponseTime time.Duration = 500 * time.Millisecond
|
|
|
|
// The main entry point of the backend.
|
|
func main() {
|
|
listenAddr := flag.String("http.addr", ":8090", "http listen address")
|
|
flag.Parse()
|
|
|
|
http.HandleFunc("/numbers", func(w http.ResponseWriter, r *http.Request) {
|
|
numbersHandler(w, r)
|
|
})
|
|
|
|
log.Fatal(http.ListenAndServe(*listenAddr, nil))
|
|
}
|
|
|
|
// 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) {
|
|
// timeout channel for the handler as a whole
|
|
timeout := make(chan bool, 1)
|
|
go func() {
|
|
time.Sleep(MaxResponseTime)
|
|
timeout <- true
|
|
}()
|
|
|
|
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 input channel for URL results.
|
|
// We will read as much as we can at once.
|
|
inputChan := make(chan []int, len(rurl))
|
|
var wg sync.WaitGroup
|
|
|
|
// fetch all URLs asynchronously
|
|
for i := range rurl {
|
|
wg.Add(1)
|
|
go func(url string) {
|
|
defer wg.Done()
|
|
n, e := getNumbers(url)
|
|
|
|
if e == nil {
|
|
if n != nil && len(n) > 0 {
|
|
inputChan <- n
|
|
} else {
|
|
log.Printf("Received empty list of numbers from endpoint")
|
|
}
|
|
} else {
|
|
log.Printf("Got an error: %s", e)
|
|
}
|
|
}(rurl[i])
|
|
}
|
|
// master routine closing the inputChan
|
|
go func() {
|
|
wg.Wait()
|
|
close(inputChan)
|
|
}()
|
|
|
|
// channel for sorting process, so we can short-circuit in
|
|
// case sorting takes too long
|
|
sortChan := make(chan []int, 1)
|
|
// aggregate numbers from URLs
|
|
var numberBuffer []int = []int{}
|
|
// these are actually sorted
|
|
var sortedNumbers []int = []int{}
|
|
|
|
// aggregate and sort loop,
|
|
// breaks if all URLs have been processed or the timeout
|
|
// has been reached
|
|
done := false
|
|
for done != true {
|
|
select {
|
|
case <-timeout:
|
|
log.Printf("Waiting for URL took too long, finishing response anyway")
|
|
finishResponse(w, sortedNumbers)
|
|
return
|
|
case res, more := <-inputChan:
|
|
if more { // still URLs to fetch
|
|
numberBuffer = append(numberBuffer, res...)
|
|
// continue to aggregate numbers from the buffer
|
|
continue
|
|
} else { // all URLs fetched, sort and be done
|
|
log.Printf("Nothing else to fetch")
|
|
done = true
|
|
}
|
|
// non-blocking branch that sorts what we already have
|
|
// we are not done here yet
|
|
default:
|
|
// only sort if we have new results
|
|
if len(numberBuffer) == 0 {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// sort fallthrough, either the inputChan is currently "empty"
|
|
// or we fetched all URLs already
|
|
go func(n []int) {
|
|
res, err := sort.SortedAndDedup(timeout, n)
|
|
if err != nil {
|
|
return
|
|
}
|
|
sortChan <- res
|
|
}(append(sortedNumbers, numberBuffer...))
|
|
numberBuffer = []int{}
|
|
|
|
select {
|
|
case merged := <-sortChan:
|
|
sortedNumbers = merged
|
|
case <-timeout:
|
|
log.Printf("Sorting took too long, finishing response anyway")
|
|
finishResponse(w, sortedNumbers)
|
|
return
|
|
}
|
|
}
|
|
|
|
log.Printf("Result is complete, finishing response")
|
|
finishResponse(w, sortedNumbers)
|
|
}
|
|
|
|
// 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) (resp []int, err error) {
|
|
// validate url
|
|
u_err := validateURL(rawurl)
|
|
if u_err != nil {
|
|
return nil, u_err
|
|
}
|
|
|
|
// retrieve response
|
|
r, r_err := http.Get(rawurl)
|
|
if r_err != nil {
|
|
return nil, r_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
|
|
}
|