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 }