Fetch independent weather sources concurrently
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -115,6 +116,175 @@ func TestFetchBundleFromFixtures(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchBundleMergesConcurrentSourcesInSourceOrder(t *testing.T) {
|
||||
paths := []string{
|
||||
"/observations",
|
||||
"/forecast/hourly",
|
||||
"/forecast/narrative",
|
||||
"/alerts/active",
|
||||
"/discussion",
|
||||
"/weatherstories/latest",
|
||||
convectiveOutlooksEndpoint,
|
||||
}
|
||||
started := make(chan string, len(paths))
|
||||
release := make(map[string]chan struct{}, len(paths))
|
||||
for _, path := range paths {
|
||||
release[path] = make(chan struct{})
|
||||
}
|
||||
var releaseOnce sync.Once
|
||||
releaseAll := func() {
|
||||
releaseOnce.Do(func() {
|
||||
for i := len(paths) - 1; i >= 0; i-- {
|
||||
close(release[paths[i]])
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Cleanup(releaseAll)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == currentConditionsEndpoint {
|
||||
if !serveWeatherFixture(w, r) {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
ready, ok := release[r.URL.Path]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
started <- r.URL.Path
|
||||
<-ready
|
||||
if !serveWeatherFixture(w, r) {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
client := newTestClient(t, server.URL+"/", nil)
|
||||
|
||||
type fetchResult struct {
|
||||
bundle *weatherdata.Bundle
|
||||
err error
|
||||
}
|
||||
result := make(chan fetchResult, 1)
|
||||
go func() {
|
||||
bundle, err := client.FetchBundle(context.Background())
|
||||
result <- fetchResult{bundle: bundle, err: err}
|
||||
}()
|
||||
|
||||
seen := make(map[string]bool, len(paths))
|
||||
for range paths {
|
||||
select {
|
||||
case path := <-started:
|
||||
seen[path] = true
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("independent requests started = %v, want %v", seen, paths)
|
||||
}
|
||||
}
|
||||
releaseAll()
|
||||
|
||||
select {
|
||||
case got := <-result:
|
||||
if got.err != nil {
|
||||
t.Fatalf("FetchBundle() error = %v", got.err)
|
||||
}
|
||||
wantSources := []string{
|
||||
config.MissingSourceObservations,
|
||||
config.MissingSourceCurrent,
|
||||
"hourly",
|
||||
config.MissingSourceNarrative,
|
||||
config.MissingSourceAlerts,
|
||||
config.MissingSourceDiscussion,
|
||||
config.MissingSourceWeatherStory,
|
||||
sourceSPCConvectiveOutlooks,
|
||||
}
|
||||
gotSources := make([]string, 0, len(got.bundle.Sources))
|
||||
for _, source := range got.bundle.Sources {
|
||||
gotSources = append(gotSources, source.Name)
|
||||
}
|
||||
if strings.Join(gotSources, ",") != strings.Join(wantSources, ",") {
|
||||
t.Fatalf("source order = %v, want %v", gotSources, wantSources)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("FetchBundle() did not finish after all source responses were released")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchBundleReportsConcurrentFailuresInSourceOrder(t *testing.T) {
|
||||
var requested []string
|
||||
server := fixtureServer(t, map[string]handlerOverride{
|
||||
"/forecast/hourly": {status: http.StatusBadRequest, body: `invalid hourly request`},
|
||||
"/forecast/narrative": {status: http.StatusBadRequest, body: `invalid narrative request`},
|
||||
}, &requested)
|
||||
client := newTestClient(t, server.URL+"/", nil)
|
||||
|
||||
_, err := client.FetchBundle(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("FetchBundle() error = nil, want source error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "/forecast/hourly") {
|
||||
t.Fatalf("error = %q, want the earlier hourly source failure", err.Error())
|
||||
}
|
||||
if !containsPath(requested, "/forecast/narrative") {
|
||||
t.Fatalf("requested paths = %v, want independent narrative request", requested)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchBundleCancelsConcurrentSourceRequests(t *testing.T) {
|
||||
paths := []string{
|
||||
"/observations",
|
||||
"/forecast/hourly",
|
||||
"/forecast/narrative",
|
||||
"/alerts/active",
|
||||
"/discussion",
|
||||
"/weatherstories/latest",
|
||||
convectiveOutlooksEndpoint,
|
||||
}
|
||||
started := make(chan string, len(paths))
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == currentConditionsEndpoint {
|
||||
if !serveWeatherFixture(w, r) {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, path := range paths {
|
||||
if r.URL.Path == path {
|
||||
started <- path
|
||||
<-r.Context().Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
client := newTestClient(t, server.URL+"/", nil)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := client.FetchBundle(ctx)
|
||||
result <- err
|
||||
}()
|
||||
for range paths {
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
cancel()
|
||||
t.Fatal("not all independent requests started before cancellation")
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case err := <-result:
|
||||
if err == nil || !strings.Contains(err.Error(), context.Canceled.Error()) {
|
||||
t.Fatalf("FetchBundle() error = %v, want context cancellation", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("FetchBundle() did not return after cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchBundleRejectsInvalidHourlyPrecipitationProbability(t *testing.T) {
|
||||
for _, probability := range []string{"-1", "101"} {
|
||||
t.Run(probability, func(t *testing.T) {
|
||||
@@ -369,8 +539,8 @@ func TestFetchRejectsOversizedResponseWithoutRetry(t *testing.T) {
|
||||
if narrativeCalls != 1 {
|
||||
t.Fatalf("narrative calls = %d, want no retry", narrativeCalls)
|
||||
}
|
||||
if containsPath(requested, "/alerts/active") {
|
||||
t.Fatalf("requested paths = %v, want source failure before later fetches", requested)
|
||||
if !containsPath(requested, "/alerts/active") {
|
||||
t.Fatalf("requested paths = %v, want independent source requests despite narrative failure", requested)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -912,21 +1082,34 @@ type handlerOverride struct {
|
||||
handler http.HandlerFunc
|
||||
}
|
||||
|
||||
var weatherFixtureFiles = map[string]string{
|
||||
"/observations": "observations.json",
|
||||
"/conditions/current": "current.json",
|
||||
"/forecast/hourly": "hourly.json",
|
||||
"/forecast/narrative": "narrative.json",
|
||||
"/alerts/active": "alerts.json",
|
||||
"/discussion": "discussion.json",
|
||||
"/weatherstories/latest": "weather_story.json",
|
||||
convectiveOutlooksEndpoint: "convective_outlooks.json",
|
||||
}
|
||||
|
||||
func serveWeatherFixture(w http.ResponseWriter, r *http.Request) bool {
|
||||
name, ok := weatherFixtureFiles[r.URL.Path]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
http.ServeFile(w, r, filepath.Join("testdata", name))
|
||||
return true
|
||||
}
|
||||
|
||||
func fixtureServer(t *testing.T, overrides map[string]handlerOverride, requested *[]string) *httptest.Server {
|
||||
t.Helper()
|
||||
fixtures := map[string]string{
|
||||
"/observations": "observations.json",
|
||||
"/conditions/current": "current.json",
|
||||
"/forecast/hourly": "hourly.json",
|
||||
"/forecast/narrative": "narrative.json",
|
||||
"/alerts/active": "alerts.json",
|
||||
"/discussion": "discussion.json",
|
||||
"/weatherstories/latest": "weather_story.json",
|
||||
convectiveOutlooksEndpoint: "convective_outlooks.json",
|
||||
}
|
||||
var requestedMu sync.Mutex
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if requested != nil {
|
||||
requestedMu.Lock()
|
||||
*requested = append(*requested, r.URL.String())
|
||||
requestedMu.Unlock()
|
||||
}
|
||||
if override, ok := overrides[r.URL.Path]; ok {
|
||||
if override.handler != nil {
|
||||
@@ -937,12 +1120,9 @@ func fixtureServer(t *testing.T, overrides map[string]handlerOverride, requested
|
||||
_, _ = w.Write([]byte(override.body))
|
||||
return
|
||||
}
|
||||
name, ok := fixtures[r.URL.Path]
|
||||
if !ok {
|
||||
if !serveWeatherFixture(w, r) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, filepath.Join("testdata", name))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
return server
|
||||
|
||||
Reference in New Issue
Block a user