Skip to content
This repository was archived by the owner on Jun 6, 2023. It is now read-only.

Commit 9ff2361

Browse files
authored
Send multiple notifications concurrently (#54)
* example of sending notifications concurrently * push: build a worker pool into Service * push: remove version of Push that does serialization and update docs * push: reorganize * push: separate queue type for async and improve handling of command line flags * test queue push (not sure how useful this test is)
1 parent 7121134 commit 9ff2361

File tree

9 files changed

+385
-110
lines changed

9 files changed

+385
-110
lines changed

README.md

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -56,43 +56,83 @@ I am still looking for feedback on the API so it may change. Please copy Buford
5656
package main
5757

5858
import (
59-
"log"
59+
"encoding/json"
60+
"fmt"
6061

6162
"github.com/RobotsAndPencils/buford/certificate"
6263
"github.com/RobotsAndPencils/buford/payload"
6364
"github.com/RobotsAndPencils/buford/payload/badge"
6465
"github.com/RobotsAndPencils/buford/push"
6566
)
6667

67-
func main() {
68-
// set these variables appropriately
69-
filename := "/path/to/certificate.p12"
70-
password := ""
71-
deviceToken := "c2732227a1d8021cfaf781d71fb2f908c61f5861079a00954a5453f1d0281433"
68+
// set these variables appropriately
69+
const (
70+
filename = "/path/to/certificate.p12"
71+
password = ""
72+
host = push.Development
73+
deviceToken = "c2732227a1d8021cfaf781d71fb2f908c61f5861079a00954a5453f1d0281433"
74+
)
7275

76+
func main() {
77+
// load a certificate and use it to connect to the APN service:
7378
cert, err := certificate.Load(filename, password)
74-
if err != nil {
75-
log.Fatal(err)
76-
}
79+
exitOnError(err)
7780

78-
service, err := push.NewService(push.Development, cert)
79-
if err != nil {
80-
log.Fatal(err)
81-
}
81+
client, err := push.NewClient(cert)
82+
exitOnError(err)
83+
84+
service := push.NewService(client, host)
8285

86+
// construct a payload to send to the device:
8387
p := payload.APS{
8488
Alert: payload.Alert{Body: "Hello HTTP/2"},
8589
Badge: badge.New(42),
8690
}
91+
b, err := json.Marshal(p)
92+
exitOnError(err)
93+
94+
// push the notification:
95+
id, err := service.Push(deviceToken, nil, b)
96+
exitOnError(err)
97+
98+
fmt.Println("apns-id:", id)
99+
}
100+
```
101+
102+
See `example/push` for the complete listing.
103+
104+
#### Concurrent use
105+
106+
HTTP/2 can send multiple requests over a single connection, but `service.Push` waits for a response before returning. Instead, you can wrap a `Service` in a queue to handle responses independently, allowing you to send multiple notifications at once.
107+
108+
```go
109+
queue := push.NewQueue(service, workers)
87110

88-
id, err := service.Push(deviceToken, nil, p)
89-
if err != nil {
90-
log.Fatal(err)
111+
// process responses
112+
go func() {
113+
for {
114+
// Response blocks until a response is available
115+
log.Println(queue.Response())
91116
}
92-
log.Println("apns-id:", id)
117+
}()
118+
119+
// send the notifications
120+
for i := 0; i < number; i++ {
121+
queue.Push(deviceToken, nil, b)
93122
}
123+
124+
// done sending notifications, wait for all responses
125+
queue.Wait()
94126
```
95127

128+
It's important to set up a goroutine to handle responses before sending any notifications, otherwise Push will block waiting for room to return a Response.
129+
130+
You can configure the number of workers used to send notifications concurrently, but be aware that a larger number isn't necessarily better, as Apple limits the number of concurrent streams. From the Apple Push Notification documentation:
131+
132+
> "The APNs server allows multiple concurrent streams for each connection. The exact number of streams is based on server load, so do not assume a specific number of streams."
133+
134+
See `example/concurrent/` for a complete listing.
135+
96136
#### Headers
97137

98138
You can specify an ID, expiration, priority, and other parameters via the Headers struct.
@@ -104,7 +144,7 @@ headers := &push.Headers{
104144
LowPriority: true,
105145
}
106146

107-
id, err := service.Push(deviceToken, headers, p)
147+
id, err := service.Push(deviceToken, headers, b)
108148
```
109149

110150
If no ID is specified APNS will generate and return a unique ID. When an expiration is specified, APNS will store and retry sending the notification until that time, otherwise APNS will not store or retry the notification. LowPriority should always be set when sending a ContentAvailable payload.
@@ -120,29 +160,17 @@ p := payload.APS{
120160
pm := p.Map()
121161
pm["acme2"] = []string{"bang", "whiz"}
122162

123-
id, err := service.Push(deviceToken, nil, pm)
124-
```
125-
126-
The Push method will use json.Marshal to serialize whatever you send it.
127-
128-
#### Resend the same payload
129-
130-
Use json.Marshal to serialize your payload once and then send it to multiple device tokens with PushBytes.
131-
132-
```go
133-
b, err := json.Marshal(p)
163+
b, err := json.Marshal(pm)
134164
if err != nil {
135-
log.Fatal(err)
165+
log.Fatal(b)
136166
}
137167

138-
id, err := service.PushBytes(deviceToken, nil, b)
168+
service.Push(deviceToken, nil, b)
139169
```
140170

141-
Whether you use Push or PushBytes, the underlying HTTP/2 connection to APNS will be reused.
142-
143171
#### Error responses
144172

145-
Push and PushBytes may return an `error`. It could be an error the JSON encoding or HTTP request, or it could be a `push.Error` which contains the response from Apple. To access the Reason and HTTP Status code, you must convert the `error` to a `push.Error` as follows:
173+
If `service.Push` or `queue.Response` returns an error, it could be an HTTP error, or it could be an error response from Apple. To access the Reason and HTTP Status code, you must convert the `error` to a `push.Error` as follows:
146174

147175
```go
148176
if e, ok := err.(*push.Error); ok {

example/concurrent/main.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"flag"
6+
"fmt"
7+
"log"
8+
"os"
9+
"time"
10+
11+
"github.com/RobotsAndPencils/buford/certificate"
12+
"github.com/RobotsAndPencils/buford/payload"
13+
"github.com/RobotsAndPencils/buford/push"
14+
)
15+
16+
func main() {
17+
log.SetFlags(log.Ltime | log.Lmicroseconds)
18+
19+
var deviceToken, filename, password, environment, host string
20+
var workers uint
21+
var number int
22+
23+
flag.StringVar(&deviceToken, "d", "", "Device token")
24+
flag.StringVar(&filename, "c", "", "Path to p12 certificate file")
25+
flag.StringVar(&password, "p", "", "Password for p12 file")
26+
flag.StringVar(&environment, "e", "development", "Environment")
27+
flag.UintVar(&workers, "w", 20, "Workers to send notifications")
28+
flag.IntVar(&number, "n", 100, "Number of notifications to send")
29+
flag.Parse()
30+
31+
// ensure required flags are set:
32+
halt := false
33+
if deviceToken == "" {
34+
fmt.Println("Device token is required.")
35+
halt = true
36+
}
37+
if filename == "" {
38+
fmt.Println("Path to .p12 certificate file is required.")
39+
halt = true
40+
}
41+
switch environment {
42+
case "development":
43+
host = push.Development
44+
case "production":
45+
host = push.Production
46+
default:
47+
fmt.Println("Environment can be development or production.")
48+
halt = true
49+
}
50+
if halt {
51+
flag.Usage()
52+
os.Exit(2)
53+
}
54+
55+
// load a certificate and use it to connect to the APN service:
56+
cert, err := certificate.Load(filename, password)
57+
exitOnError(err)
58+
59+
client, err := push.NewClient(cert)
60+
exitOnError(err)
61+
service := push.NewService(client, host)
62+
queue := push.NewQueue(service, workers)
63+
64+
// process responses
65+
go func() {
66+
count := 1
67+
for {
68+
id, device, err := queue.Response()
69+
if err != nil {
70+
log.Printf("(%d) device: %s, error: %v", count, device, err)
71+
} else {
72+
log.Printf("(%d) device: %s, apns-id: %s", count, device, id)
73+
}
74+
count++
75+
}
76+
}()
77+
78+
// prepare notification(s) to send
79+
p := payload.APS{
80+
Alert: payload.Alert{Body: "Hello HTTP/2"},
81+
}
82+
b, err := json.Marshal(p)
83+
exitOnError(err)
84+
85+
// send notifications:
86+
start := time.Now()
87+
for i := 0; i < number; i++ {
88+
queue.Push(deviceToken, nil, b)
89+
}
90+
// done sending notifications, wait for all responses:
91+
queue.Wait()
92+
elapsed := time.Since(start)
93+
94+
log.Printf("Time for %d responses: %s (%s ea.)", number, elapsed, elapsed/time.Duration(number))
95+
}
96+
97+
func exitOnError(err error) {
98+
if err != nil {
99+
fmt.Println(err)
100+
os.Exit(1)
101+
}
102+
}

example/push/main.go

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"flag"
5-
"log"
6+
"fmt"
7+
"os"
68

79
"github.com/RobotsAndPencils/buford/certificate"
810
"github.com/RobotsAndPencils/buford/payload"
@@ -11,35 +13,65 @@ import (
1113
)
1214

1315
func main() {
14-
var deviceToken, filename, password, environment string
16+
var deviceToken, filename, password, environment, host string
1517

1618
flag.StringVar(&deviceToken, "d", "", "Device token")
17-
flag.StringVar(&filename, "c", "", "Path to p12 certificate file")
18-
flag.StringVar(&password, "p", "", "Password for p12 file.")
19+
flag.StringVar(&filename, "c", "", "Path to .p12 certificate file.")
20+
flag.StringVar(&password, "p", "", "Password for .p12 file.")
1921
flag.StringVar(&environment, "e", "development", "Environment")
2022
flag.Parse()
2123

22-
cert, err := certificate.Load(filename, password)
23-
if err != nil {
24-
log.Fatal(err)
24+
// ensure required flags are set:
25+
halt := false
26+
if deviceToken == "" {
27+
fmt.Println("Device token is required.")
28+
halt = true
2529
}
26-
27-
service, err := push.NewService(push.Development, cert)
28-
if err != nil {
29-
log.Fatal(err)
30+
if filename == "" {
31+
fmt.Println("Path to .p12 certificate file is required.")
32+
halt = true
33+
}
34+
switch environment {
35+
case "development":
36+
host = push.Development
37+
case "production":
38+
host = push.Production
39+
default:
40+
fmt.Println("Environment can be development or production.")
41+
halt = true
3042
}
31-
if environment == "production" {
32-
service.Host = push.Production
43+
if halt {
44+
flag.Usage()
45+
os.Exit(2)
3346
}
3447

48+
// load a certificate and use it to connect to the APN service:
49+
cert, err := certificate.Load(filename, password)
50+
exitOnError(err)
51+
52+
client, err := push.NewClient(cert)
53+
exitOnError(err)
54+
55+
service := push.NewService(client, host)
56+
57+
// construct a payload to send to the device:
3558
p := payload.APS{
3659
Alert: payload.Alert{Body: "Hello HTTP/2"},
3760
Badge: badge.New(42),
3861
}
62+
b, err := json.Marshal(p)
63+
exitOnError(err)
64+
65+
// push the notification:
66+
id, err := service.Push(deviceToken, nil, b)
67+
exitOnError(err)
68+
69+
fmt.Println("apns-id:", id)
70+
}
3971

40-
id, err := service.Push(deviceToken, &push.Headers{}, p)
72+
func exitOnError(err error) {
4173
if err != nil {
42-
log.Fatal(err)
74+
fmt.Println(err)
75+
os.Exit(1)
4376
}
44-
log.Println("apns-id:", id)
4577
}

example/website/main.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,12 @@ func pushHandler(w http.ResponseWriter, r *http.Request) {
5555
// URLArgs must match placeholders in URLFormatString
5656
URLArgs: []string{"hello"},
5757
}
58+
b, err := json.Marshal(p)
59+
if err != nil {
60+
log.Fatal(err)
61+
}
5862

59-
id, err := service.Push(deviceToken, nil, p)
63+
id, err := service.Push(deviceToken, nil, b)
6064
if err != nil {
6165
log.Println(err)
6266
return
@@ -140,11 +144,13 @@ func main() {
140144
log.Fatal(err)
141145
}
142146

143-
service, err = push.NewService(push.Production, cert)
147+
client, err := push.NewClient(cert)
144148
if err != nil {
145149
log.Fatal(err)
146150
}
147151

152+
service = push.NewService(client, push.Production)
153+
148154
r := mux.NewRouter()
149155
r.HandleFunc("/", indexHandler).Methods("GET")
150156
r.HandleFunc("/request", requestPermissionHandler)

0 commit comments

Comments
 (0)