func (s *endpointServiceImpl) send(ctx context.Context, data []byte) error { ctx = log.SetField(ctx, "endpointURL", s.url) return retryCall(ctx, "endpoint.send", func() error { startTime := clock.Now(ctx) log.Debugf(ctx, "Pushing message to endpoint.") req, err := http.NewRequest("POST", s.url, bytes.NewReader(data)) if err != nil { log.Errorf(log.SetError(ctx, err), "Failed to create HTTP request.") return err } req.Header.Add("content-type", protobufContentType) req.Header.Add("user-agent", monitoringEndpointUserAgent) resp, err := s.client.Do(req) if err != nil { // Treat a client error as transient. log.Warningf(log.SetError(ctx, err), "Failed proxy client request.") return errors.WrapTransient(err) } defer resp.Body.Close() // Read the full response body. This will enable us to re-use the // connection. bodyData, err := ioutil.ReadAll(resp.Body) if err != nil { log.Errorf(log.SetError(ctx, err), "Error during endpoint connection.") return errors.WrapTransient(err) } log.Fields{ "status": resp.Status, "statusCode": resp.StatusCode, "headers": resp.Header, "contentLength": resp.ContentLength, "body": string(bodyData), "duration": clock.Now(ctx).Sub(startTime), }.Debugf(ctx, "Received HTTP response from endpoint.") if http.StatusOK <= resp.StatusCode && resp.StatusCode < http.StatusMultipleChoices { log.Debugf(ctx, "Message pushed successfully.") return nil } err = fmt.Errorf("http: server error (%d)", resp.StatusCode) if resp.StatusCode >= http.StatusInternalServerError { err = errors.WrapTransient(err) } log.Fields{ log.ErrorKey: err, "status": resp.Status, "statusCode": resp.StatusCode, }.Warningf(ctx, "Proxy error.") return err }) }
// wrapTransient examines the supplied error. If it's not a recognized error // value, it is treated as transient. // // This is because, at the moment, the transiant nature of the pubsub return // codes is not discernable, so we will error on the side of caution (retry). func (*pubsubClient) wrapTransient(err error) error { switch err { case nil: return nil case context.Canceled: return err default: return errors.WrapTransient(err) } }
func TestPubSub(t *testing.T) { t.Parallel() Convey(`Using a testing Pub/Sub config`, t, func() { // Do not retry. ctx := context.WithValue(context.Background(), backoffPolicyKey, func() retry.Iterator { return &retry.Limited{} }) config := pubsubConfig{ project: "test-project", topic: "test-topic", subscription: "test-subscription", create: true, batchSize: 64, } svc := &testPubSubService{} defer So(svc, mock.ShouldHaveNoErrors) Convey(`When the subscription does not exist`, func() { svc.MockCall("SubExists", "test-subscription").WithResult(false, nil) Convey(`And the topic does not exist, will create a new topic and subscription.`, func() { svc.MockCall("TopicExists", "test-topic").WithResult(false, nil) svc.MockCall("CreateTopic", "test-topic").WithResult(nil) svc.MockCall("CreatePullSub", "test-subscription", "test-topic").WithResult(nil) _, err := newPubSubClient(ctx, config, svc) So(err, ShouldBeNil) }) Convey(`And the topic exists, will create a new subscription.`, func() { svc.MockCall("TopicExists", "test-topic").WithResult(true, nil) svc.MockCall("CreatePullSub", "test-subscription", "test-topic").WithResult(nil) _, err := newPubSubClient(ctx, config, svc) So(err, ShouldBeNil) }) Convey(`Will fail to create a new client when "create" is false.`, func() { config.create = false _, err := newPubSubClient(ctx, config, svc) So(err, ShouldNotBeNil) }) }) Convey(`Will create a new client.`, func() { svc.MockCall("SubExists", "test-subscription").WithResult(true, nil) client, err := newPubSubClient(ctx, config, svc) So(err, ShouldBeNil) Convey(`When executing pull/ack with no messages`, func() { svc.MockCall("Pull", "test-subscription", 64).WithResult(nil, nil) Convey(`Returns errNoMessages.`, func() { err := client.pullAckMessages(ctx, func([]*pubsub.Message) {}) So(err, ShouldEqual, errNoMessages) }) }) Convey(`When executing pull/ack with one message`, func() { msgs := []*pubsub.Message{ { ID: "id0", AckID: "ack0", Data: []byte{0xd0, 0x65}, }, } svc.MockCall("Pull", "test-subscription", 64).WithResult(msgs, nil) Convey(`Returns and ACKs that message.`, func() { svc.MockCall("Ack", "test-subscription", []string{"ack0"}).WithResult(nil) var pullMsg []*pubsub.Message err := client.pullAckMessages(ctx, func(msg []*pubsub.Message) { pullMsg = msg }) So(err, ShouldBeNil) So(pullMsg, ShouldResemble, msgs) }) Convey(`ACKs the message even if the handler panics.`, func() { svc.MockCall("Ack", "test-subscription", []string{"ack0"}).WithResult(nil) So(func() { client.pullAckMessages(ctx, func(msg []*pubsub.Message) { panic("Handler failure!") }) }, ShouldPanic) }) Convey(`Does not ACK the message if the handler clears it.`, func() { err := client.pullAckMessages(ctx, func(msg []*pubsub.Message) { for i := range msg { msg[i] = nil } }) So(err, ShouldBeNil) }) }) Convey(`When executing pull/ack with an error`, func() { e := errors.New("TEST ERROR") svc.MockCall("Pull", "test-subscription", 64).WithResult(nil, e) Convey(`Returns the error as transient.`, func() { So(client.pullAckMessages(ctx, func([]*pubsub.Message) {}), ShouldResemble, errors.WrapTransient(e)) }) }) }) }) }