Skip to content

Commit bce5eaf

Browse files
authored
Process /api/v2/remove-ineligible-domains in shards (#279)
Processing all domains at once is failing to complete. Adding query parameters to the endpoint to have it process a subset of the domains on each run should result in each run using less resources and taking less time. The cron configuration is changed to split the work into 4 approximately evenly sized shards.
1 parent 5590f33 commit bce5eaf

File tree

7 files changed

+299
-12
lines changed

7 files changed

+299
-12
lines changed

api/domain_handlers.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ func (api API) Remove(w http.ResponseWriter, r *http.Request) {
404404

405405
type DomainStateWithIssues struct {
406406
DomainState database.DomainState
407-
Issues hstspreload.Issues
407+
Issues hstspreload.Issues
408408
}
409409

410410
// RemoveIneligibleDomains runs eligibility checks on domains present in the
@@ -429,14 +429,22 @@ func (api API) RemoveIneligibleDomains(w http.ResponseWriter, r *http.Request) {
429429
var deleteEligibleDomains []string
430430

431431
api.logger.Print("Fetching domains...")
432-
// Get all domains
433-
domains, err := api.database.AllDomainStates()
432+
var start, end string
433+
if s, ok := r.URL.Query()["start"]; ok && len(s) > 0 {
434+
start = s[0]
435+
}
436+
if e, ok := r.URL.Query()["end"]; ok && len(e) > 0 {
437+
end = e[0]
438+
}
439+
// Get domains
440+
api.logger.Printf("using start %q, end %q", start, end)
441+
domains, err := api.database.DomainStatesInRange(start, end)
434442
if err != nil {
435443
msg := fmt.Sprintf("Internal error: could not retrieve domains. (%s)\n", err)
436444
http.Error(w, msg, http.StatusInternalServerError)
437445
return
438446
}
439-
api.logger.Print("Filtering domains...")
447+
api.logger.Printf("Filtering %d domains...", len(domains))
440448

441449
// Filter Domains
442450
for _, d := range domains {
@@ -446,7 +454,7 @@ func (api API) RemoveIneligibleDomains(w http.ResponseWriter, r *http.Request) {
446454
}
447455
api.logger.Print("Getting ineligible domain states...")
448456

449-
// call GetIneligibleDomainStates, add to map
457+
// call GetIneligibleDomainStates and store them in a map by domain name
450458
states := make(map[string]database.IneligibleDomainState)
451459
state, err := api.database.GetAllIneligibleDomainStates()
452460
if err != nil {
@@ -455,10 +463,15 @@ func (api API) RemoveIneligibleDomains(w http.ResponseWriter, r *http.Request) {
455463
return
456464
}
457465

458-
// delete domains that exist in the ineligible database but not
459-
// on the preload list
460466
for _, s := range state {
467+
// ignore IneligibleDomainStates for domain names not in the [start, end)
468+
// range we're processing.
469+
if (start != "" && s.Name < start) || (end != "" && s.Name >= end) {
470+
continue
471+
}
461472
states[s.Name] = s
473+
// Delete IneligibleDomainStates for names that are no longer on the
474+
// preload list.
462475
if _, ok := policyStates[s.Name]; !ok {
463476
deleteEligibleDomains = append(deleteEligibleDomains, s.Name)
464477
}
@@ -488,7 +501,7 @@ func (api API) RemoveIneligibleDomains(w http.ResponseWriter, r *http.Request) {
488501
i++
489502
wg.Add(1)
490503
domainStates <- d
491-
if i % 1000 == 0 {
504+
if i%1000 == 0 {
492505
api.logger.Printf("Sent %d domains to workers\n", i)
493506
}
494507
}

api/domain_handlers_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,130 @@ func TestDeletionAndStatusChange(t *testing.T) {
363363
t.Errorf("Status changed")
364364
}
365365
}
366+
367+
func TestRemoveIneligibleDomainsSharding(t *testing.T) {
368+
api, _, mockHstspreload, mockPreloadlist := mockAPI(0 * time.Second)
369+
370+
testPreloadlist := preloadlist.PreloadList{Entries: []preloadlist.Entry{
371+
{Name: "a.test", Mode: preloadlist.ForceHTTPS, IncludeSubDomains: true, Policy: preloadlist.Bulk1Year},
372+
{Name: "n.test", Mode: preloadlist.ForceHTTPS, IncludeSubDomains: true, Policy: preloadlist.Bulk1Year},
373+
{Name: "z.test", Mode: preloadlist.ForceHTTPS, IncludeSubDomains: true, Policy: preloadlist.Bulk1Year},
374+
}}
375+
testEligibleResponses := map[string]hstspreload.Issues{
376+
"a.test": issuesWithErrors,
377+
"n.test": issuesWithErrors,
378+
"z.test": issuesWithErrors,
379+
}
380+
mockPreloadlist.list = testPreloadlist
381+
mockHstspreload.eligibleResponses = testEligibleResponses
382+
383+
w := httptest.NewRecorder()
384+
w.Body = &bytes.Buffer{}
385+
386+
r, err := http.NewRequest("GET", "", nil)
387+
if err != nil {
388+
t.Fatalf("[%s] %s", "NewRequest Failed", err)
389+
}
390+
391+
api.Update(w, r)
392+
393+
// These test cases are structured to be run in this specific order and
394+
// each case depends on the behavior of the previous ones.
395+
tests := []struct {
396+
name string
397+
query string
398+
expectedCounts map[string]int
399+
}{
400+
{
401+
// Start by running RemoveIneligibleDomains with no query
402+
// parameters - it should process every domain.
403+
"no range specified",
404+
"",
405+
map[string]int{
406+
"a.test": 1,
407+
"n.test": 1,
408+
"z.test": 1,
409+
},
410+
},
411+
{
412+
// Specifying an end of "n" (the [start, end) interval is half-open)
413+
// should result in only a.test being processed. Every time a domain
414+
// is processed, the number of scans in its IneligibleDomainState
415+
// increases.
416+
"query range only has end",
417+
"end=n",
418+
map[string]int{
419+
"a.test": 2,
420+
"n.test": 1,
421+
"z.test": 1,
422+
},
423+
},
424+
{
425+
// With an interval of ["n","z"), only n.test should match.
426+
"start and end",
427+
"start=n&end=z",
428+
map[string]int{
429+
"a.test": 2,
430+
"n.test": 2,
431+
"z.test": 1,
432+
},
433+
},
434+
{
435+
// A start of "z" with no end should only match z.test from the
436+
// test preload list.
437+
"only start",
438+
"start=z",
439+
map[string]int{
440+
"a.test": 2,
441+
"n.test": 2,
442+
"z.test": 2,
443+
},
444+
},
445+
{
446+
// A bad range (start after end) does nothing.
447+
"bad range",
448+
"start=b&end=a",
449+
map[string]int{
450+
"a.test": 2,
451+
"n.test": 2,
452+
"z.test": 2,
453+
},
454+
},
455+
}
456+
457+
for _, test := range tests {
458+
// Make request with the specified query
459+
r, err := http.NewRequest("GET", "", nil)
460+
if err != nil {
461+
t.Fatalf("[%s] %s", "NewRequest Failed", err)
462+
}
463+
r = toAppEngineHttpRequest(r)
464+
r.URL.RawQuery = test.query
465+
w := httptest.NewRecorder()
466+
w.Body = &bytes.Buffer{}
467+
api.RemoveIneligibleDomains(w, r)
468+
469+
// Look at the IneligibleDomainStates created or updated by
470+
// RemoveIneligibleDomains and check that the number of scans for
471+
// each domain matches the expected count.
472+
states, err := api.database.GetAllIneligibleDomainStates()
473+
if err != nil {
474+
t.Fatalf("Couldn't get the states of all domains in the database.")
475+
}
476+
seenNames := make(map[string]bool)
477+
for _, state := range states {
478+
expectedCount, found := test.expectedCounts[state.Name]
479+
if !found {
480+
t.Errorf("[%s] found unexpected domain %q in IneligibleDomainStates list", test.name, state.Name)
481+
continue
482+
}
483+
if len(state.Scans) != expectedCount {
484+
t.Errorf("[%s] Unexpected number of scans for domain %q: got %d, want %d", test.name, state.Name, len(state.Scans), expectedCount)
485+
}
486+
seenNames[state.Name] = true
487+
}
488+
if len(seenNames) != len(test.expectedCounts) {
489+
t.Errorf("[%s] Wrong number of IneligibleDomainStates: got %d, want %d", test.name, len(seenNames), len(test.expectedCounts))
490+
}
491+
}
492+
}

cron.yaml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
cron:
2-
- description: "Removes ineligible domains"
3-
url: "/api/v2/remove-ineligible-domains"
4-
schedule: every monday 09:00
2+
- description: "Remove ineligible domains ['','e')"
3+
url: "/api/v2/remove-ineligible-domains?end=e"
4+
schedule: every monday 9:00
5+
timezone: America/New_York
6+
- description: "Remove ineligible domains ['e','l')"
7+
url: "/api/v2/remove-ineligible-domains?start=e&end=l"
8+
schedule: every monday 11:00
9+
timezone: America/New_York
10+
- description: "Remove ineligible domains ['l','s')"
11+
url: "/api/v2/remove-ineligible-domains?start=l&end=s"
12+
schedule: every monday 13:00
13+
timezone: America/New_York
14+
- description: "Remove ineligible domains ['s','')"
15+
url: "/api/v2/remove-ineligible-domains?start=s"
16+
schedule: every monday 15:00
517
timezone: America/New_York
6-

database/database.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Database interface {
2727
StateForDomain(string) (DomainState, error)
2828
StatesForDomains([]string) ([]DomainState, error)
2929
AllDomainStates() ([]DomainState, error)
30+
DomainStatesInRange(start, end string) ([]DomainState, error)
3031
StatesWithStatus(PreloadStatus) ([]DomainState, error)
3132
GetIneligibleDomainStates(domains []string) (states []IneligibleDomainState, err error)
3233
SetIneligibleDomainStates(updates []IneligibleDomainState, logf func(format string, args ...interface{})) error
@@ -208,6 +209,17 @@ func (db DatastoreBacked) AllDomainStates() (states []DomainState, err error) {
208209
return db.statesForQuery(datastore.NewQuery("DomainState"))
209210
}
210211

212+
func (db DatastoreBacked) DomainStatesInRange(start, end string) ([]DomainState, error) {
213+
query := datastore.NewQuery(domainStateKind)
214+
if start != "" {
215+
query = query.FilterField("__key__", ">=", datastore.NameKey(domainStateKind, start, nil))
216+
}
217+
if end != "" {
218+
query = query.FilterField("__key__", "<", datastore.NameKey(domainStateKind, end, nil))
219+
}
220+
return db.statesForQuery(query)
221+
}
222+
211223
// StatesWithStatus returns the states of domains with the given status in the database.
212224
func (db DatastoreBacked) StatesWithStatus(status PreloadStatus) (domains []DomainState, err error) {
213225
return db.statesForQuery(

database/database_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,100 @@ func TestStatesWithStatus(t *testing.T) {
276276
}
277277
}
278278

279+
func TestDomainStatesInRange(t *testing.T) {
280+
resetDB()
281+
282+
testNames := []string{
283+
"1.test",
284+
"a.test",
285+
"aaa.test",
286+
"b.test",
287+
"g.test",
288+
"h.test",
289+
"y.test",
290+
"z.test",
291+
}
292+
for _, name := range testNames {
293+
state := DomainState{Name: name}
294+
if err := testDB.PutState(state); err != nil {
295+
t.Fatalf("failed to set test state for TestDomainStatesInRange: %v", err)
296+
}
297+
}
298+
299+
tests := []struct {
300+
name string
301+
start string
302+
end string
303+
expectedNames []string
304+
}{
305+
{
306+
"empty start and end",
307+
"",
308+
"",
309+
testNames,
310+
},
311+
{
312+
"empty start",
313+
"",
314+
"a",
315+
[]string{"1.test"},
316+
},
317+
{
318+
"empty end",
319+
"z",
320+
"",
321+
[]string{"z.test"},
322+
},
323+
{
324+
"intervals are half-open",
325+
"a",
326+
"b",
327+
[]string{"a.test", "aaa.test"},
328+
},
329+
{
330+
"larger interval",
331+
"a",
332+
"h",
333+
[]string{"a.test", "aaa.test", "b.test", "g.test"},
334+
},
335+
{
336+
"same non-empty start and end returns empty list",
337+
"a.test",
338+
"a.test",
339+
nil,
340+
},
341+
{
342+
"start after end returns empty list",
343+
"b",
344+
"a",
345+
nil,
346+
},
347+
}
348+
349+
for _, test := range tests {
350+
t.Run(test.name, func(t *testing.T) {
351+
states, err := testDB.DomainStatesInRange(test.start, test.end)
352+
if err != nil {
353+
t.Fatalf("DomainStatesInRange unexpectedly failed: %v", err)
354+
}
355+
// check that the names in the returned states match the ones in test.expectedNames
356+
nameSet := make(map[string]bool)
357+
for _, state := range states {
358+
nameSet[state.Name] = true
359+
}
360+
for _, name := range test.expectedNames {
361+
if !nameSet[name] {
362+
t.Errorf("%q was not returned by DomainStatesInRange but should have been", name)
363+
}
364+
delete(nameSet, name)
365+
}
366+
for name := range nameSet {
367+
t.Errorf("%q was returned by DomainStatesInRange but not expected", name)
368+
}
369+
})
370+
}
371+
}
372+
279373
func TestSetPendingAutomatedRemoval(t *testing.T) {
280374
resetDB()
281375

database/gcd/gcd.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gcd
33
import (
44
"errors"
55
"fmt"
6+
"io"
67
"net"
78
"net/http"
89
"os"
@@ -102,6 +103,7 @@ func NewLocalBackend() (db LocalBackend, shutdown func() error, err error) {
102103
"--testing",
103104
)
104105
db.cmd = cmd
106+
stderr, _ := cmd.StderrPipe()
105107

106108
err = cmd.Start()
107109
if err != nil {
@@ -127,6 +129,19 @@ func NewLocalBackend() (db LocalBackend, shutdown func() error, err error) {
127129
}
128130
}
129131

132+
// try to read some from stderr to see if we can get a more useful error message:
133+
if stderr != nil {
134+
msg := make([]byte, 2048)
135+
n, err := stderr.Read(msg)
136+
if err != nil && err != io.EOF {
137+
return db, shutdown, fmt.Errorf("failed to connect, failure reading stderr: %v", err)
138+
}
139+
msg = msg[:n]
140+
if strings.Contains(string(msg), "Unable to locate a Java Runtime.") {
141+
return db, shutdown, fmt.Errorf("failed to connect, unable to locate Java runtime to run datastore emulator")
142+
}
143+
}
144+
130145
return db, shutdown, fmt.Errorf("could not connect")
131146
}
132147

0 commit comments

Comments
 (0)