-
Notifications
You must be signed in to change notification settings - Fork 131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enforced Google's specification regarding back-off mode #78
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,17 +17,25 @@ package safebrowsing | |
import ( | ||
"bytes" | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io/ioutil" | ||
"math/rand" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
"time" | ||
|
||
pb "github.com/google/safebrowsing/internal/safebrowsing_proto" | ||
|
||
"github.com/golang/protobuf/proto" | ||
) | ||
|
||
const ( | ||
maxRetryDelay = 24 * time.Hour | ||
baseRetryDelay = 15 * time.Minute | ||
) | ||
|
||
const ( | ||
findHashPath = "/v4/fullHashes:find" | ||
fetchUpdatePath = "/v4/threatListUpdates:fetch" | ||
|
@@ -41,8 +49,10 @@ type api interface { | |
|
||
// netAPI is an api object that talks to the server over HTTP. | ||
type netAPI struct { | ||
client *http.Client | ||
url *url.URL | ||
client *http.Client | ||
url *url.URL | ||
backOffMode bool | ||
updateAPIErrors uint | ||
} | ||
|
||
// newNetAPI creates a new netAPI object pointed at the provided root URL. | ||
|
@@ -72,13 +82,17 @@ func newNetAPI(root string, key string, proxy string) (*netAPI, error) { | |
q.Set("key", key) | ||
q.Set("alt", "proto") | ||
u.RawQuery = q.Encode() | ||
return &netAPI{url: u, client: httpClient}, nil | ||
return &netAPI{url: u, client: httpClient, backOffMode: false}, nil | ||
} | ||
|
||
// doRequests performs a POST to requestPath. It uses the marshaled form of req | ||
// as the request body payload, and automatically unmarshals the response body | ||
// payload as resp. | ||
func (a *netAPI) doRequest(ctx context.Context, requestPath string, req proto.Message, resp proto.Message) error { | ||
if a.backOffMode { | ||
return errors.New("API in back-off mode, refusing to execute") | ||
} | ||
|
||
p, err := proto.Marshal(req) | ||
if err != nil { | ||
return err | ||
|
@@ -95,8 +109,25 @@ func (a *netAPI) doRequest(ctx context.Context, requestPath string, req proto.Me | |
} | ||
defer httpResp.Body.Close() | ||
if httpResp.StatusCode != 200 { | ||
// Non 200 response, we shall enter back-off mode | ||
a.backOffMode = true | ||
go func(a *netAPI) { | ||
// Backoff strategy: MIN((2**N-1 * 15 minutes) * (RAND + 1), 24 hours) | ||
n := 1 << a.updateAPIErrors | ||
delay := time.Duration(float64(n) * (rand.Float64() + 1) * float64(baseRetryDelay)) | ||
if delay > maxRetryDelay { | ||
delay = maxRetryDelay | ||
} | ||
a.updateAPIErrors++ | ||
time.Sleep(delay) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of a sleeping goroutine, how about we have an error count, and a 'time we can make requests again', along with a RWMutex. So at the top, it would just do:
then later:
Obviously doesn't have to be exactly like this. This way makes it possible to export the 'time we can make requests again', whether via an error, or more directly through a field. |
||
a.backOffMode = false | ||
}(a) | ||
|
||
return fmt.Errorf("safebrowsing: unexpected server response code: %d", httpResp.StatusCode) | ||
} | ||
|
||
a.updateAPIErrors = 0 | ||
|
||
body, err := ioutil.ReadAll(httpResp.Body) | ||
if err != nil { | ||
return err | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -552,38 +552,15 @@ func TestDatabaseUpdate(t *testing.T) { | |
t.Fatalf("update 5, database state mismatch:\ngot %+v\nwant %+v", gotDB, wantDB) | ||
} | ||
|
||
// Update 6: api is broken for some unknown reason. Checks the backoff | ||
// Update 6: api is broken for some unknown reason. Checks retrial delay. | ||
errResponse = errors.New("Something broke") | ||
delay, updated = db.Update(context.Background(), mockAPI) | ||
if db.err == nil || updated { | ||
t.Fatalf("update 6, unexpected update success") | ||
} | ||
minDelay := baseRetryDelay.Seconds() * float64(1) * float64(1) | ||
maxDelay := baseRetryDelay.Seconds() * float64(2) * float64(1) | ||
if delay.Seconds() < minDelay || delay.Seconds() > maxDelay { | ||
t.Fatalf("update 6, Expected delay %v to be between %v and %v", delay.Seconds(), minDelay, maxDelay) | ||
} | ||
|
||
// Update 7: api is still broken, check backoff is larger | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should have tests somewhere for the exponential backoff. Probably in api_test.go (that test is a bit ugly right now). I also notice that the API doesn't get the 'Now()' from the configuration, which may make it difficult to write time based tests. |
||
delay, updated = db.Update(context.Background(), mockAPI) | ||
if db.err == nil || updated { | ||
t.Fatalf("update 7, unexpected update success") | ||
} | ||
minDelay = baseRetryDelay.Seconds() * float64(1) * float64(2) | ||
maxDelay = baseRetryDelay.Seconds() * float64(2) * float64(2) | ||
if delay.Seconds() < minDelay || delay.Seconds() > maxDelay { | ||
t.Fatalf("update 7, Expected delay %v to be between %v and %v", delay.Seconds(), minDelay, maxDelay) | ||
} | ||
|
||
// Update 8: api is still broken, check that backoff is larger than before | ||
delay, updated = db.Update(context.Background(), mockAPI) | ||
if db.err == nil || updated { | ||
t.Fatalf("update 8, unexpected update success") | ||
} | ||
minDelay = baseRetryDelay.Seconds() * float64(1) * float64(4) | ||
maxDelay = baseRetryDelay.Seconds() * float64(2) * float64(4) | ||
if delay.Seconds() < minDelay || delay.Seconds() > maxDelay { | ||
t.Fatalf("update 8, Expected delay %v to be between %v and %v", delay.Seconds(), minDelay, maxDelay) | ||
expectedDelay = 1 * time.Minute | ||
if delay != expectedDelay { | ||
t.Fatalf("update 6, Expected delay %v to be %v", delay.Seconds(), expectedDelay.Seconds) | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lock is needed for this. The 'database' was handling locking before, but now we have HashLookup requests happening too (which can happen concurrently).