Skip to content

Commit

Permalink
feat(fcm): SendEach and SendEachForMulticast for FCM batch send (#552)
Browse files Browse the repository at this point in the history
* Implement `SendEach`, `SendEachDryRun`, `SendEachForMulticast`, (#544)

`SendEachForMulticastDryRun`

1. Add `SendEach`, `SendEachDryRun`, `SendEachForMulticast`,
   `SendEachForMulticastDryRun`
2. Deprecate `SendAll`, `SendAllDryRun`, `SendMulticast`,
   `SendMulticastDryRun`

`SendEach` vs `SendAll`
1. `SendEach` sends one HTTP request to V1 Send endpoint for each
    message in the array.
   `SendAll` sends only one HTTP request to V1 Batch Send endpoint
    to send all messages in the array.
2. `SendEach` calls fcmClient.Send to send each message
    and constructs a SendResponse with the returned message id or error.
    `SendEach` uses sync.WaitGroup to execute all fcmClient.Send calls
    asynchronously and wait for all of them to complete and construct a
    BatchResponse with all SendResponses.
    Therefore, unlike `SendAll`, `SendEach` does not always returns
    an error for a total failure. It can also return a `BatchResponse`
    with only errors in it.

`SendEachForMulticast` calls `SendEach` under the hood.

* Add integration tests for `SendEach` and `SendEachForMulticast` (#550)

* Avoid using "-- i.e." in the function comments

* Remove all backticks in messaging_batch.go
  • Loading branch information
Doris-Ge committed Apr 21, 2023
1 parent d6f889a commit 3fb1538
Show file tree
Hide file tree
Showing 3 changed files with 724 additions and 13 deletions.
124 changes: 124 additions & 0 deletions integration/messaging/messaging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,130 @@ func TestSendInvalidToken(t *testing.T) {
}
}

func TestSendEach(t *testing.T) {
messages := []*messaging.Message{
{
Notification: &messaging.Notification{
Title: "Title 1",
Body: "Body 1",
},
Topic: "foo-bar",
},
{
Notification: &messaging.Notification{
Title: "Title 2",
Body: "Body 2",
},
Topic: "foo-bar",
},
{
Notification: &messaging.Notification{
Title: "Title 3",
Body: "Body 3",
},
Token: "INVALID_TOKEN",
},
}

br, err := client.SendEachDryRun(context.Background(), messages)
if err != nil {
t.Fatal(err)
}

if len(br.Responses) != 3 {
t.Errorf("len(Responses) = %d; want = 3", len(br.Responses))
}
if br.SuccessCount != 2 {
t.Errorf("SuccessCount = %d; want = 2", br.SuccessCount)
}
if br.FailureCount != 1 {
t.Errorf("FailureCount = %d; want = 1", br.FailureCount)
}

for i := 0; i < 2; i++ {
sr := br.Responses[i]
if err := checkSuccessfulSendResponse(sr); err != nil {
t.Errorf("Responses[%d]: %v", i, err)
}
}

sr := br.Responses[2]
if sr.Success {
t.Errorf("Responses[2]: Success = true; want = false")
}
if sr.MessageID != "" {
t.Errorf("Responses[2]: MessageID = %q; want = %q", sr.MessageID, "")
}
if sr.Error == nil || !messaging.IsInvalidArgument(sr.Error) {
t.Errorf("Responses[2]: Error = %v; want = InvalidArgumentError", sr.Error)
}
}

func TestSendEachFiveHundred(t *testing.T) {
var messages []*messaging.Message
const limit = 500
for i := 0; i < limit; i++ {
m := &messaging.Message{
Topic: fmt.Sprintf("foo-bar-%d", i%10),
}
messages = append(messages, m)
}

br, err := client.SendEachDryRun(context.Background(), messages)
if err != nil {
t.Fatal(err)
}

if len(br.Responses) != limit {
t.Errorf("len(Responses) = %d; want = %d", len(br.Responses), limit)
}
if br.SuccessCount != limit {
t.Errorf("SuccessCount = %d; want = %d", br.SuccessCount, limit)
}
if br.FailureCount != 0 {
t.Errorf("FailureCount = %d; want = 0", br.FailureCount)
}

for i := 0; i < limit; i++ {
sr := br.Responses[i]
if err := checkSuccessfulSendResponse(sr); err != nil {
t.Errorf("Responses[%d]: %v", i, err)
}
}
}

func TestSendEachForMulticast(t *testing.T) {
message := &messaging.MulticastMessage{
Notification: &messaging.Notification{
Title: "title",
Body: "body",
},
Tokens: []string{"INVALID_TOKEN", "ANOTHER_INVALID_TOKEN"},
}

br, err := client.SendEachForMulticastDryRun(context.Background(), message)
if err != nil {
t.Fatal(err)
}

if len(br.Responses) != 2 {
t.Errorf("len(Responses) = %d; want = 2", len(br.Responses))
}
if br.SuccessCount != 0 {
t.Errorf("SuccessCount = %d; want = 0", br.SuccessCount)
}
if br.FailureCount != 2 {
t.Errorf("FailureCount = %d; want = 2", br.FailureCount)
}

for i := 0; i < 2; i++ {
sr := br.Responses[i]
if err := checkErrorSendResponse(sr); err != nil {
t.Errorf("Responses[%d]: %v", i, err)
}
}
}

func TestSendAll(t *testing.T) {
messages := []*messaging.Message{
{
Expand Down
157 changes: 144 additions & 13 deletions messaging/messaging_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"mime/multipart"
"net/http"
"net/textproto"
"sync"

"firebase.google.com/go/v4/internal"
)
Expand Down Expand Up @@ -80,21 +81,145 @@ type SendResponse struct {
Error error
}

// BatchResponse represents the response from the `SendAll()` and `SendMulticast()` APIs.
// BatchResponse represents the response from the SendAll() and SendMulticast() APIs.
type BatchResponse struct {
SuccessCount int
FailureCount int
Responses []*SendResponse
}

// SendEach sends the messages in the given array via Firebase Cloud Messaging.
//
// The messages array may contain up to 500 messages. Unlike SendAll(), SendEach sends the entire
// array of messages by making a single HTTP call for each message. The responses list
// obtained from the return value corresponds to the order of the input messages. An error
// from SendEach or a BatchResponse with all failures indicates a total failure, meaning that
// none of the messages in the list could be sent. Partial failures or no failures are only
// indicated by a BatchResponse return value.
func (c *fcmClient) SendEach(ctx context.Context, messages []*Message) (*BatchResponse, error) {
return c.sendEachInBatch(ctx, messages, false)
}

// SendEachDryRun sends the messages in the given array via Firebase Cloud Messaging in the
// dry run (validation only) mode.
//
// This function does not actually deliver any messages to target devices. Instead, it performs all
// the SDK-level and backend validations on the messages, and emulates the send operation.
//
// The messages array may contain up to 500 messages. Unlike SendAllDryRun(), SendEachDryRun sends
// the entire array of messages by making a single HTTP call for each message. The responses list
// obtained from the return value corresponds to the order of the input messages. An error
// from SendEachDryRun or a BatchResponse with all failures indicates a total failure, meaning
// that none of the messages in the list could be sent. Partial failures or no failures are only
// indicated by a BatchResponse return value.
func (c *fcmClient) SendEachDryRun(ctx context.Context, messages []*Message) (*BatchResponse, error) {
return c.sendEachInBatch(ctx, messages, true)
}

// SendMulticast sends the given multicast message to all the FCM registration tokens specified.
//
// The tokens array in MulticastMessage may contain up to 500 tokens. SendMulticast uses the
// SendEach() function to send the given message to all the target recipients. The
// responses list obtained from the return value corresponds to the order of the input tokens. An error
// from SendEachForMulticast or a BatchResponse with all failures indicates a total failure, meaning
// that none of the messages in the list could be sent. Partial failures or no failures are only
// indicated by a BatchResponse return value.
func (c *fcmClient) SendEachForMulticast(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) {
messages, err := toMessages(message)
if err != nil {
return nil, err
}

return c.SendEach(ctx, messages)
}

// SendEachForMulticastDryRun sends the given multicast message to all the specified FCM registration
// tokens in the dry run (validation only) mode.
//
// This function does not actually deliver any messages to target devices. Instead, it performs all
// the SDK-level and backend validations on the messages, and emulates the send operation.
//
// The tokens array in MulticastMessage may contain up to 500 tokens. SendEachForMulticastDryRunn uses the
// SendEachDryRun() function to send the given message. The responses list obtained from
// the return value corresponds to the order of the input tokens. An error from SendEachForMulticastDryRun
// or a BatchResponse with all failures indicates a total failure, meaning that of the messages in the
// list could be sent. Partial failures or no failures are only
// indicated by a BatchResponse return value.
func (c *fcmClient) SendEachForMulticastDryRun(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) {
messages, err := toMessages(message)
if err != nil {
return nil, err
}

return c.SendEachDryRun(ctx, messages)
}

func (c *fcmClient) sendEachInBatch(ctx context.Context, messages []*Message, dryRun bool) (*BatchResponse, error) {
if len(messages) == 0 {
return nil, errors.New("messages must not be nil or empty")
}

if len(messages) > maxMessages {
return nil, fmt.Errorf("messages must not contain more than %d elements", maxMessages)
}

var responses []*SendResponse = make([]*SendResponse, len(messages))
var wg sync.WaitGroup

for idx, m := range messages {
if err := validateMessage(m); err != nil {
return nil, fmt.Errorf("invalid message at index %d: %v", idx, err)
}
wg.Add(1)
go func(idx int, m *Message, dryRun bool, responses []*SendResponse) {
defer wg.Done()
var resp string
var err error
if dryRun {
resp, err = c.SendDryRun(ctx, m)
} else {
resp, err = c.Send(ctx, m)
}
if err == nil {
responses[idx] = &SendResponse{
Success: true,
MessageID: resp,
}
} else {
responses[idx] = &SendResponse{
Success: false,
Error: err,
}
}
}(idx, m, dryRun, responses)
}
// Wait for all SendDryRun/Send calls to finish
wg.Wait()

successCount := 0
for _, r := range responses {
if r.Success {
successCount++
}
}

return &BatchResponse{
Responses: responses,
SuccessCount: successCount,
FailureCount: len(responses) - successCount,
}, nil
}

// SendAll sends the messages in the given array via Firebase Cloud Messaging.
//
// The messages array may contain up to 500 messages. SendAll employs batching to send the entire
// array of mssages as a single RPC call. Compared to the `Send()` function,
// array of messages as a single RPC call. Compared to the Send() function,
// this is a significantly more efficient way to send multiple messages. The responses list
// obtained from the return value corresponds to the order of the input messages. An error from
// SendAll indicates a total failure -- i.e. none of the messages in the array could be sent.
// Partial failures are indicated by a `BatchResponse` return value.
// SendAll indicates a total failure, meaning that none of the messages in the array could be
// sent. Partial failures are indicated by a BatchResponse return value.
//
// Deprecated: Use SendEach instead.
func (c *fcmClient) SendAll(ctx context.Context, messages []*Message) (*BatchResponse, error) {
return c.sendBatch(ctx, messages, false)
}
Expand All @@ -106,22 +231,26 @@ func (c *fcmClient) SendAll(ctx context.Context, messages []*Message) (*BatchRes
// the SDK-level and backend validations on the messages, and emulates the send operation.
//
// The messages array may contain up to 500 messages. SendAllDryRun employs batching to send the
// entire array of mssages as a single RPC call. Compared to the `SendDryRun()` function, this
// entire array of messages as a single RPC call. Compared to the SendDryRun() function, this
// is a significantly more efficient way to validate sending multiple messages. The responses list
// obtained from the return value corresponds to the order of the input messages. An error from
// SendAllDryRun indicates a total failure -- i.e. none of the messages in the array could be sent
// for validation. Partial failures are indicated by a `BatchResponse` return value.
// SendAllDryRun indicates a total failure, meaning that none of the messages in the array could
// be sent for validation. Partial failures are indicated by a BatchResponse return value.
//
// Deprecated: Use SendEachDryRun instead.
func (c *fcmClient) SendAllDryRun(ctx context.Context, messages []*Message) (*BatchResponse, error) {
return c.sendBatch(ctx, messages, true)
}

// SendMulticast sends the given multicast message to all the FCM registration tokens specified.
//
// The tokens array in MulticastMessage may contain up to 500 tokens. SendMulticast uses the
// `SendAll()` function to send the given message to all the target recipients. The
// SendAll() function to send the given message to all the target recipients. The
// responses list obtained from the return value corresponds to the order of the input tokens. An
// error from SendMulticast indicates a total failure -- i.e. the message could not be sent to any
// of the recipients. Partial failures are indicated by a `BatchResponse` return value.
// error from SendMulticast indicates a total failure, meaning that the message could not be sent
// to any of the recipients. Partial failures are indicated by a BatchResponse return value.
//
// Deprecated: Use SendEachForMulticast instead.
func (c *fcmClient) SendMulticast(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) {
messages, err := toMessages(message)
if err != nil {
Expand All @@ -138,10 +267,12 @@ func (c *fcmClient) SendMulticast(ctx context.Context, message *MulticastMessage
// the SDK-level and backend validations on the messages, and emulates the send operation.
//
// The tokens array in MulticastMessage may contain up to 500 tokens. SendMulticastDryRun uses the
// `SendAllDryRun()` function to send the given message. The responses list obtained from
// SendAllDryRun() function to send the given message. The responses list obtained from
// the return value corresponds to the order of the input tokens. An error from SendMulticastDryRun
// indicates a total failure -- i.e. none of the messages were sent to FCM for validation. Partial
// failures are indicated by a `BatchResponse` return value.
// indicates a total failure, meaning that none of the messages were sent to FCM for validation.
// Partial failures are indicated by a BatchResponse return value.
//
// Deprecated: Use SendEachForMulticastDryRun instead.
func (c *fcmClient) SendMulticastDryRun(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) {
messages, err := toMessages(message)
if err != nil {
Expand Down
Loading

0 comments on commit 3fb1538

Please sign in to comment.