Skip to content

Commit 6af7fab

Browse files
authored
Implement rate limiters for every Postman client function that uses a GET request (#3964)
* Made a second rate limiter for the general rate limit and moved limiters to the functions that make the GET request * Updated tests
1 parent 95a2a2a commit 6af7fab

File tree

3 files changed

+67
-20
lines changed

3 files changed

+67
-20
lines changed

pkg/sources/postman/postman.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ .
197197
continue
198198
}
199199

200-
collection, err := s.client.GetCollection(collectionID)
200+
collection, err := s.client.GetCollection(ctx, collectionID)
201201
if err != nil {
202202
return fmt.Errorf("error getting collection %s: %w", collectionID, err)
203203
}
@@ -260,7 +260,7 @@ func (s *Source) scanWorkspace(ctx context.Context, chunksChan chan *sources.Chu
260260

261261
// gather and scan environment variables
262262
for _, envID := range workspace.Environments {
263-
envVars, err := s.client.GetEnvironmentVariables(envID.UUID)
263+
envVars, err := s.client.GetEnvironmentVariables(ctx, envID.UUID)
264264
if err != nil {
265265
ctx.Logger().Error(err, "could not get env variables", "environment_uuid", envID.UUID)
266266
continue
@@ -297,7 +297,7 @@ func (s *Source) scanWorkspace(ctx context.Context, chunksChan chan *sources.Chu
297297
if shouldSkip(collectionID.UUID, s.conn.IncludeCollections, s.conn.ExcludeCollections) {
298298
continue
299299
}
300-
collection, err := s.client.GetCollection(collectionID.UUID)
300+
collection, err := s.client.GetCollection(ctx, collectionID.UUID)
301301
if err != nil {
302302
return err
303303
}

pkg/sources/postman/postman_client.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,12 @@ type Client struct {
187187
// Headers to attach to every requests made with the client.
188188
Headers map[string]string
189189

190-
// Rate limiter needed for Postman API workspace enumeration. Postman API general rate limit 300 requests per minute
191-
// but is 10 calls in 10 seconds for GET /collections, GET /workspaces, and GET /workspaces/{id} endpoints.
192-
WorkspaceEnumerationRateLimiter *rate.Limiter
190+
// Rate limiter needed for Postman API workspace and collection requests. Postman API rate limit
191+
// is 10 calls in 10 seconds for GET /collections, GET /workspaces, and GET /workspaces/{id} endpoints.
192+
WorkspaceAndCollectionRateLimiter *rate.Limiter
193193

194-
// TODO: Add rate limiter(s) for Postman API calls inside of other loops
194+
// Rate limiter needed for Postman API. General rate limit is 300 requests per minute.
195+
GeneralRateLimiter *rate.Limiter
195196
}
196197

197198
// NewClient returns a new Postman API client.
@@ -203,9 +204,10 @@ func NewClient(postmanToken string) *Client {
203204
}
204205

205206
c := &Client{
206-
HTTPClient: http.DefaultClient,
207-
Headers: bh,
208-
WorkspaceEnumerationRateLimiter: rate.NewLimiter(rate.Every(time.Second), 1),
207+
HTTPClient: http.DefaultClient,
208+
Headers: bh,
209+
WorkspaceAndCollectionRateLimiter: rate.NewLimiter(rate.Every(time.Second), 1),
210+
GeneralRateLimiter: rate.NewLimiter(rate.Every(time.Second/5), 1),
209211
}
210212

211213
return c
@@ -281,9 +283,6 @@ func (c *Client) EnumerateWorkspaces(ctx context.Context) ([]Workspace, error) {
281283
}
282284

283285
for i, workspace := range workspacesObj.Workspaces {
284-
if err := c.WorkspaceEnumerationRateLimiter.Wait(ctx); err != nil {
285-
return nil, fmt.Errorf("could not wait for rate limiter during workspace enumeration: %w", err)
286-
}
287286
tempWorkspace, err := c.GetWorkspace(ctx, workspace.ID)
288287
if err != nil {
289288
return nil, fmt.Errorf("could not get workspace %q (%s) during enumeration: %w", workspace.Name, workspace.ID, err)
@@ -304,6 +303,9 @@ func (c *Client) GetWorkspace(ctx context.Context, workspaceUUID string) (Worksp
304303
}{}
305304

306305
url := fmt.Sprintf(WORKSPACE_URL, workspaceUUID)
306+
if err := c.WorkspaceAndCollectionRateLimiter.Wait(ctx); err != nil {
307+
return Workspace{}, fmt.Errorf("could not wait for rate limiter during workspace getting: %w", err)
308+
}
307309
r, err := c.getPostmanReq(url, nil)
308310
if err != nil {
309311
return Workspace{}, fmt.Errorf("could not get workspace (%s): %w", workspaceUUID, err)
@@ -323,12 +325,15 @@ func (c *Client) GetWorkspace(ctx context.Context, workspaceUUID string) (Worksp
323325
}
324326

325327
// GetEnvironmentVariables returns the environment variables for a given environment
326-
func (c *Client) GetEnvironmentVariables(environment_uuid string) (VariableData, error) {
328+
func (c *Client) GetEnvironmentVariables(ctx context.Context, environment_uuid string) (VariableData, error) {
327329
obj := struct {
328330
VariableData VariableData `json:"environment"`
329331
}{}
330332

331333
url := fmt.Sprintf(ENVIRONMENTS_URL, environment_uuid)
334+
if err := c.GeneralRateLimiter.Wait(ctx); err != nil {
335+
return VariableData{}, fmt.Errorf("could not wait for rate limiter during environment variable getting: %w", err)
336+
}
332337
r, err := c.getPostmanReq(url, nil)
333338
if err != nil {
334339
return VariableData{}, fmt.Errorf("could not get env variables for environment (%s): %w", environment_uuid, err)
@@ -347,12 +352,15 @@ func (c *Client) GetEnvironmentVariables(environment_uuid string) (VariableData,
347352
}
348353

349354
// GetCollection returns the collection for a given collection
350-
func (c *Client) GetCollection(collection_uuid string) (Collection, error) {
355+
func (c *Client) GetCollection(ctx context.Context, collection_uuid string) (Collection, error) {
351356
obj := struct {
352357
Collection Collection `json:"collection"`
353358
}{}
354359

355360
url := fmt.Sprintf(COLLECTIONS_URL, collection_uuid)
361+
if err := c.WorkspaceAndCollectionRateLimiter.Wait(ctx); err != nil {
362+
return Collection{}, fmt.Errorf("could not wait for rate limiter during collection getting: %w", err)
363+
}
356364
r, err := c.getPostmanReq(url, nil)
357365
if err != nil {
358366
return Collection{}, fmt.Errorf("could not get collection (%s): %w", collection_uuid, err)

pkg/sources/postman/postman_test.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func TestSource_ScanVariableData(t *testing.T) {
242242
}
243243
}
244244

245-
func TestSource_ScanEnumerateRateLimit(t *testing.T) {
245+
func TestSource_ScanEnumerateWorkspaceRateLimit(t *testing.T) {
246246
defer gock.Off()
247247
// Mock the API response for workspaces
248248
numWorkspaces := 3
@@ -265,9 +265,9 @@ func TestSource_ScanEnumerateRateLimit(t *testing.T) {
265265
Get(fmt.Sprintf("/workspaces/%d", i)).
266266
Reply(200).
267267
BodyString(fmt.Sprintf(`{"workspace":{"id":"%d","name":"workspace-%d","type":"personal","description":"Test workspace number %d",
268-
"visibility":"personal","createdBy":"1234","updatedBy":"1234","createdAt":"2024-12-12T23:32:27.000Z","updatedAt":"2024-12-12T23:33:01.000Z",
269-
"collections":[{"id":"abc%d","name":"test-collection-1","uid":"1234-abc%d"},{"id":"def%d","name":"test-collection-2","uid":"1234-def%d"}],
270-
"environments":[{"id":"ghi%d","name":"test-environment-1","uid":"1234-ghi%d"},{"id":"jkl%d","name":"test-environment-2","uid":"1234-jkl%d"}]}}`, i, i, i, i, i, i, i, i, i, i, i))
268+
"visibility":"personal","createdBy":"1234","updatedBy":"1234","createdAt":"2024-12-12T23:32:27.000Z","updatedAt":"2024-12-12T23:33:01.000Z",
269+
"collections":[{"id":"abc%d","name":"test-collection-1","uid":"1234-abc%d"},{"id":"def%d","name":"test-collection-2","uid":"1234-def%d"}],
270+
"environments":[{"id":"ghi%d","name":"test-environment-1","uid":"1234-ghi%d"},{"id":"jkl%d","name":"test-environment-2","uid":"1234-jkl%d"}]}}`, i, i, i, i, i, i, i, i, i, i, i))
271271
}
272272

273273
ctx := context.Background()
@@ -281,6 +281,7 @@ func TestSource_ScanEnumerateRateLimit(t *testing.T) {
281281
t.Fatalf("init error: %v", err)
282282
}
283283
gock.InterceptClient(s.client.HTTPClient)
284+
defer gock.RestoreClient(s.client.HTTPClient)
284285

285286
start := time.Now()
286287
_, err = s.client.EnumerateWorkspaces(ctx)
@@ -291,6 +292,44 @@ func TestSource_ScanEnumerateRateLimit(t *testing.T) {
291292
// With <numWorkspaces> requests at 1 per second rate limit,
292293
// elapsed time should be at least <numWorkspaces - 1> seconds
293294
if elapsed < time.Duration(numWorkspaces-1)*time.Second {
294-
t.Errorf("Rate limiting not working as expected. Elapsed time: %v, expected at least %d seconds", elapsed, numWorkspaces-1)
295+
t.Errorf("Rate limiting not working as expected. Elapsed time: %v seconds, expected at least %d seconds", elapsed.Seconds(), numWorkspaces-1)
296+
}
297+
}
298+
299+
func TestSource_ScanGeneralRateLimit(t *testing.T) {
300+
defer gock.Off()
301+
// Mock the API response for a specific environment id
302+
gock.New("https://api.getpostman.com").
303+
Get("environments/abc").
304+
Persist().
305+
Reply(200).
306+
BodyString(`{"environment":{"uid":"1234-abc","id":"abc","name":"test-environment","owner":"1234","createdAt":"2025-02-13T23:17:36.000Z",
307+
"updatedAt":"2025-02-13T23:18:14.000Z","values":[{"key":"test-key","value":"a-secret-value","enabled":true,"type":"default"}],"isPublic":false}}`)
308+
ctx := context.Background()
309+
s, conn := createTestSource(&sourcespb.Postman{
310+
Credential: &sourcespb.Postman_Token{
311+
Token: "super-secret-token",
312+
},
313+
})
314+
err := s.Init(ctx, "test - postman", 0, 1, false, conn, 1)
315+
if err != nil {
316+
t.Fatalf("init error: %v", err)
317+
}
318+
gock.InterceptClient(s.client.HTTPClient)
319+
defer gock.RestoreClient(s.client.HTTPClient)
320+
321+
numRequests := 3
322+
start := time.Now()
323+
for i := 0; i < numRequests; i++ {
324+
_, err = s.client.GetEnvironmentVariables(ctx, "abc")
325+
if err != nil {
326+
t.Fatalf("get environment variables error: %v", err)
327+
}
328+
}
329+
elapsed := time.Since(start)
330+
// With number of requests at 5 per second rate limit,
331+
// elapsed time should be at least <(numRequests - 1)/5> seconds
332+
if elapsed < time.Duration((numRequests-1)/5)*time.Second {
333+
t.Errorf("Rate limiting not working as expected. Elapsed time: %v seconds, expected at least %v seconds", elapsed.Seconds(), (float64(numRequests)-1)/5)
295334
}
296335
}

0 commit comments

Comments
 (0)