go-challenge/src/numbers/numbers.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
}