Skip to content

Commit e42f05d

Browse files
imhoffdsideshow
authored andcommitted
Rename ClientPool to ClientManager
1 parent 30ae022 commit e42f05d

File tree

5 files changed

+302
-301
lines changed

5 files changed

+302
-301
lines changed

client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type Client struct {
5252
// connection and disconnection as a denial-of-service attack.
5353
//
5454
// If your use case involves multiple long-lived connections, consider using
55-
// the ClientPool, which manages connections for you.
55+
// the ClientManager, which manages clients for you.
5656
func NewClient(certificate tls.Certificate) *Client {
5757
tlsConfig := &tls.Config{
5858
Certificates: []tls.Certificate{certificate},

client_manager.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package apns2
2+
3+
import (
4+
"container/list"
5+
"crypto/sha1"
6+
"crypto/tls"
7+
"sync"
8+
"time"
9+
)
10+
11+
type managerItem struct {
12+
key [sha1.Size]byte
13+
client *Client
14+
lastUsed time.Time
15+
}
16+
17+
// ClientManager is a way to manage multiple connections to the APNs.
18+
type ClientManager struct {
19+
// MaxSize is the maximum number of clients allowed in the manager. When
20+
// this limit is reached, the least recently used client is evicted. Set
21+
// zero for no limit.
22+
MaxSize int
23+
24+
// MaxAge is the maximum age of clients in the manager. Upon retrieval, if
25+
// a client has remained unused in the manager for this duration or longer,
26+
// it is evicted and nil is returned. Set zero to disable this
27+
// functionality.
28+
MaxAge time.Duration
29+
30+
// Factory is the function which constructs clients if not found in the
31+
// manager.
32+
Factory func(certificate tls.Certificate) *Client
33+
34+
cache map[[sha1.Size]byte]*list.Element
35+
ll *list.List
36+
mu sync.Mutex
37+
}
38+
39+
// NewClientManager returns a new ClientManager for prolonged, concurrent usage
40+
// of multiple APNs clients. ClientManager is flexible enough to work best for
41+
// your use case. When a client is not found in the manager, Get will return
42+
// the result of calling Factory, which can be a Client or nil.
43+
//
44+
// Having multiple clients per certificate in the manager is not allowed.
45+
//
46+
// By default, MaxSize is 64, MaxAge is 10 minutes, and Factory always returns
47+
// a Client with default options.
48+
func NewClientManager() *ClientManager {
49+
manager := &ClientManager{
50+
MaxSize: 64,
51+
MaxAge: 10 * time.Minute,
52+
Factory: NewClient,
53+
}
54+
55+
manager.initInternals()
56+
57+
return manager
58+
}
59+
60+
// Add adds a Client to the manager. You can use this to individually configure
61+
// Clients in the manager.
62+
func (m *ClientManager) Add(client *Client) {
63+
if m.cache == nil {
64+
m.initInternals()
65+
}
66+
m.mu.Lock()
67+
defer m.mu.Unlock()
68+
key := cacheKey(client.Certificate)
69+
now := time.Now()
70+
if ele, hit := m.cache[key]; hit {
71+
item := ele.Value.(*managerItem)
72+
item.client = client
73+
item.lastUsed = now
74+
m.ll.MoveToFront(ele)
75+
return
76+
}
77+
ele := m.ll.PushFront(&managerItem{key, client, now})
78+
m.cache[key] = ele
79+
if m.MaxSize != 0 && m.ll.Len() > m.MaxSize {
80+
m.mu.Unlock()
81+
m.removeOldest()
82+
m.mu.Lock()
83+
}
84+
}
85+
86+
// Get gets a Client from the manager. If a Client is not found in the manager
87+
// or if a Client has remained in the manager longer than MaxAge, Get will call
88+
// the ClientManager's Factory function, store the result in the manager if
89+
// non-nil, and return it.
90+
func (m *ClientManager) Get(certificate tls.Certificate) *Client {
91+
if m.cache == nil {
92+
m.initInternals()
93+
}
94+
m.mu.Lock()
95+
defer m.mu.Unlock()
96+
key := cacheKey(certificate)
97+
now := time.Now()
98+
if ele, hit := m.cache[key]; hit {
99+
item := ele.Value.(*managerItem)
100+
if m.MaxAge != 0 && item.lastUsed.Before(now.Add(-m.MaxAge)) {
101+
c := m.Factory(certificate)
102+
if c == nil {
103+
return nil
104+
}
105+
item.client = c
106+
}
107+
item.lastUsed = now
108+
m.ll.MoveToFront(ele)
109+
return item.client
110+
}
111+
112+
c := m.Factory(certificate)
113+
if c == nil {
114+
return nil
115+
}
116+
m.mu.Unlock()
117+
m.Add(c)
118+
m.mu.Lock()
119+
return c
120+
}
121+
122+
// Len returns the current size of the ClientManager.
123+
func (m *ClientManager) Len() int {
124+
if m.cache == nil {
125+
return 0
126+
}
127+
m.mu.Lock()
128+
defer m.mu.Unlock()
129+
return m.ll.Len()
130+
}
131+
132+
func (m *ClientManager) initInternals() {
133+
m.cache = map[[sha1.Size]byte]*list.Element{}
134+
m.ll = list.New()
135+
m.mu = sync.Mutex{}
136+
}
137+
138+
func (m *ClientManager) removeOldest() {
139+
m.mu.Lock()
140+
ele := m.ll.Back()
141+
m.mu.Unlock()
142+
if ele != nil {
143+
m.removeElement(ele)
144+
}
145+
}
146+
147+
func (m *ClientManager) removeElement(e *list.Element) {
148+
m.mu.Lock()
149+
defer m.mu.Unlock()
150+
m.ll.Remove(e)
151+
delete(m.cache, e.Value.(*managerItem).key)
152+
}
153+
154+
func cacheKey(certificate tls.Certificate) [sha1.Size]byte {
155+
var data []byte
156+
157+
for _, cert := range certificate.Certificate {
158+
data = append(data, cert...)
159+
}
160+
161+
return sha1.Sum(data)
162+
}

client_manager_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package apns2_test
2+
3+
import (
4+
"bytes"
5+
"crypto/tls"
6+
"reflect"
7+
"testing"
8+
"time"
9+
10+
"github.com/sideshow/apns2"
11+
"github.com/sideshow/apns2/certificate"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestNewClientManager(t *testing.T) {
16+
manager := apns2.NewClientManager()
17+
assert.Equal(t, manager.MaxSize, 64)
18+
assert.Equal(t, manager.MaxAge, 10*time.Minute)
19+
}
20+
21+
func TestClientManagerGetWithoutNew(t *testing.T) {
22+
manager := apns2.ClientManager{
23+
MaxSize: 32,
24+
MaxAge: 5 * time.Minute,
25+
Factory: apns2.NewClient,
26+
}
27+
28+
c1 := manager.Get(mockCert())
29+
c2 := manager.Get(mockCert())
30+
v1 := reflect.ValueOf(c1)
31+
v2 := reflect.ValueOf(c2)
32+
assert.NotNil(t, c1)
33+
assert.Equal(t, v1.Pointer(), v2.Pointer())
34+
assert.Equal(t, 1, manager.Len())
35+
}
36+
37+
func TestClientManagerAddWithoutNew(t *testing.T) {
38+
manager := apns2.ClientManager{
39+
MaxSize: 32,
40+
MaxAge: 5 * time.Minute,
41+
Factory: apns2.NewClient,
42+
}
43+
44+
manager.Add(apns2.NewClient(mockCert()))
45+
assert.Equal(t, 1, manager.Len())
46+
}
47+
48+
func TestClientManagerLenWithoutNew(t *testing.T) {
49+
manager := apns2.ClientManager{
50+
MaxSize: 32,
51+
MaxAge: 5 * time.Minute,
52+
Factory: apns2.NewClient,
53+
}
54+
55+
assert.Equal(t, 0, manager.Len())
56+
}
57+
58+
func TestClientManagerGetDefaultOptions(t *testing.T) {
59+
manager := apns2.NewClientManager()
60+
c1 := manager.Get(mockCert())
61+
c2 := manager.Get(mockCert())
62+
v1 := reflect.ValueOf(c1)
63+
v2 := reflect.ValueOf(c2)
64+
assert.NotNil(t, c1)
65+
assert.Equal(t, v1.Pointer(), v2.Pointer())
66+
assert.Equal(t, 1, manager.Len())
67+
}
68+
69+
func TestClientManagerGetNilClientFactory(t *testing.T) {
70+
manager := apns2.NewClientManager()
71+
manager.Factory = func(certificate tls.Certificate) *apns2.Client {
72+
return nil
73+
}
74+
c1 := manager.Get(mockCert())
75+
c2 := manager.Get(mockCert())
76+
assert.Nil(t, c1)
77+
assert.Nil(t, c2)
78+
assert.Equal(t, 0, manager.Len())
79+
}
80+
81+
func TestClientManagerGetMaxAgeExpiration(t *testing.T) {
82+
manager := apns2.NewClientManager()
83+
manager.MaxAge = time.Nanosecond
84+
c1 := manager.Get(mockCert())
85+
time.Sleep(time.Microsecond)
86+
c2 := manager.Get(mockCert())
87+
v1 := reflect.ValueOf(c1)
88+
v2 := reflect.ValueOf(c2)
89+
assert.NotNil(t, c1)
90+
assert.NotEqual(t, v1.Pointer(), v2.Pointer())
91+
assert.Equal(t, 1, manager.Len())
92+
}
93+
94+
func TestClientManagerGetMaxAgeExpirationWithNilFactory(t *testing.T) {
95+
manager := apns2.NewClientManager()
96+
manager.Factory = func(certificate tls.Certificate) *apns2.Client {
97+
return nil
98+
}
99+
manager.MaxAge = time.Nanosecond
100+
manager.Add(apns2.NewClient(mockCert()))
101+
c1 := manager.Get(mockCert())
102+
time.Sleep(time.Microsecond)
103+
c2 := manager.Get(mockCert())
104+
assert.Nil(t, c1)
105+
assert.Nil(t, c2)
106+
assert.Equal(t, 1, manager.Len())
107+
}
108+
109+
func TestClientManagerGetMaxSizeExceeded(t *testing.T) {
110+
manager := apns2.NewClientManager()
111+
manager.MaxSize = 1
112+
cert1 := mockCert()
113+
_ = manager.Get(cert1)
114+
cert2, _ := certificate.FromP12File("certificate/_fixtures/certificate-valid.p12", "")
115+
_ = manager.Get(cert2)
116+
cert3, _ := certificate.FromP12File("certificate/_fixtures/certificate-valid-encrypted.p12", "password")
117+
c := manager.Get(cert3)
118+
assert.True(t, bytes.Equal(cert3.Certificate[0], c.Certificate.Certificate[0]))
119+
assert.Equal(t, 1, manager.Len())
120+
}
121+
122+
func TestClientManagerAdd(t *testing.T) {
123+
fn := func(certificate tls.Certificate) *apns2.Client {
124+
t.Fatal("factory should not have been called")
125+
return nil
126+
}
127+
128+
manager := apns2.NewClientManager()
129+
manager.Factory = fn
130+
manager.Add(apns2.NewClient(mockCert()))
131+
manager.Get(mockCert())
132+
}
133+
134+
func TestClientManagerAddTwice(t *testing.T) {
135+
manager := apns2.NewClientManager()
136+
manager.Add(apns2.NewClient(mockCert()))
137+
manager.Add(apns2.NewClient(mockCert()))
138+
assert.Equal(t, 1, manager.Len())
139+
}

0 commit comments

Comments
 (0)