Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [5.0.2] - 2025-06-30
### Added
- Backoff and jitter configuration for retries via environment variables:
- `MERAKI_RETRIES`: Maximum number of retries (default: 3)
- `MERAKI_RETRY_DELAY`: Base wait time between retries in ms (default: 1000)
- `MERAKI_RETRY_JITTER`: Maximum random jitter in ms (default: 3000)
- `MERAKI_USE_RETRY_HEADER`: Whether to respect Retry-After header (default: false)


## [5.0.1] - 2025-01-20
### Fixed
- Update `IMEI` field type in `ResponseItemOrganizationsGetOrganizationDevices` to float64.
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,48 @@ Client, err = meraki.NewClient()
devicesCount, _, err := Client.Devices.GetDeviceCount()
```

## Backoff and Jitter Configuration

The client allows you to configure the retry (backoff) strategy with jitter to avoid collisions when retrying failed requests. This can be adjusted using environment variables or directly in the code.

### Environment Variables

- `MERAKI_RETRIES`: Maximum number of retries (default: 3)
- `MERAKI_RETRY_DELAY`: Base wait time between retries, in milliseconds (default: 1000 ms)
- `MERAKI_RETRY_JITTER`: Maximum random jitter added to the backoff, in milliseconds (default: 3000 ms)
- `MERAKI_USE_RETRY_HEADER`: If set to `true`, the client will respect the `Retry-After` header from API responses (default: false)

Example:

```sh
export MERAKI_RETRIES=5
export MERAKI_RETRY_DELAY=2000
export MERAKI_RETRY_JITTER=5000
export MERAKI_USE_RETRY_HEADER=true
```

### How does backoff with jitter work?

On each retry, the SDK adds to the base wait time (`MERAKI_RETRY_DELAY`) a random value between 0 and `MERAKI_RETRY_JITTER`, to avoid multiple clients retrying at the same time:

```
wait_time = RETRY_DELAY + random(0, RETRY_JITTER)
```

If `MERAKI_USE_RETRY_HEADER` is set to `true`, the client will also respect the `Retry-After` header sent by the API, if present, to determine the wait time before the next retry.

### Default values

- `MERAKI_RETRIES`: **3**
- `MERAKI_RETRY_DELAY`: **1000 ms**
- `MERAKI_RETRY_JITTER`: **3000 ms**
- `MERAKI_USE_RETRY_HEADER`: **false**

If the environment variables are not set, these default values will be used.

---


## Examples

Here is an example of how we can generate a client, get a device count and then a list of devices filtering them using query params.
Expand Down
4 changes: 4 additions & 0 deletions examples/GetAllTests/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ func main() {
return
}

// nResponse, _, err := client.Organizations.GetOrganizationDevices("828099381482762270", &meraki.GetOrganizationDevicesQueryParams{
// PerPage: -1,
// TagsFilterType: "withAnyTags",
// })
nResponse, _, err := client.Organizations.GetOrganizationDevices("828099381482762270", &meraki.GetOrganizationDevicesQueryParams{
PerPage: -1,
TagsFilterType: "withAnyTags",
Expand Down
16 changes: 16 additions & 0 deletions examples/GetAllTests2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,20 @@ func main() {
}

fmt.Println("There's no data on response")

// nResponse, _, err := client.Networks.GetNetworkClientsApplicationUsageWithRetries("L_828099381482771185", &meraki.GetNetworkClientsApplicationUsageQueryParams{
// PerPage: 10,
// Clients: "4c:c8:a1:0b:01:58",
// })

// if err != nil {
// fmt.Println(err)
// return
// }
// if nResponse != nil {
// fmt.Println("\n Cantidad: ", len(*nResponse))
// fmt.Printf("%v", *nResponse)
// return
// }
// fmt.Println("There's no data on response")
}
4 changes: 3 additions & 1 deletion examples/devices/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ func main() {
return
}

nResponse, _, err := client.Devices.GetDeviceClients("QBSC-ALSL-3GXN", nil)
nResponse, _, err := client.Devices.GetDeviceClients("QBSC-ALSL-3GXN", &meraki.GetDeviceClientsQueryParams{
T0: "2025-06-24T00:00:00Z",
})

if err != nil {
fmt.Println(err)
Expand Down
72 changes: 72 additions & 0 deletions examples/testing/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package main

import (
"fmt"
"sync"

meraki "github.com/meraki/dashboard-api-go/v5/sdk"
)

func main() {

var err error
client, err := meraki.NewClient()
if err != nil {
fmt.Println(err)
return
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
// Do work.
getAdministeredIDentitiesMe(client)
wg.Done()
}()
go func() {
// Do work.
getAdministeredIDentitiesMe(client)
wg.Done()
}()
wg.Wait()

// r, resp, err := client.Organizations.GetOrganizationFloorPlansAutoLocateDevices("828099381482762270", &meraki.GetOrganizationFloorPlansAutoLocateDevicesQueryParams{
// PerPage: 100,
// })
// if err != nil {
// fmt.Println(err)
// return
// }
// getAdministeredIDentitiesMe(client)
// r, resp, err := client.Administered.GenerateAdministeredIDentitiesMeAPIKeysWithRetries()
// if err != nil {
// fmt.Println(err)
// return
// }

// r, resp, err := client.Devices.BulkUpdateOrganizationDevicesDetailsWithRetries("828099381482762270", &meraki.RequestDevicesBulkUpdateOrganizationDevicesDetails{
// Details: &[]meraki.RequestDevicesBulkUpdateOrganizationDevicesDetailsDetails{
// {
// Name: "name",
// Value: "value",
// },
// },
// Serials: []string{"QBSC-ALSL-3GXN"},
// })
// fmt.Println(r)
// fmt.Println(resp)
// r, resp, err := client.Administered.GetAdministeredIDentitiesMeWithRetries()
// getAdministeredIDentitiesMe(client)
}

func getAdministeredIDentitiesMe(client *meraki.Client) {
for {
r, resp, err := client.Administered.GetAdministeredIDentitiesMe()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(r)
fmt.Println(resp)
}

}
8 changes: 4 additions & 4 deletions examples/wireless/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func main() {
return
}

nResponse, _, err := client.Wireless.GetOrganizationWirelessDevicesSystemCpuLoadHistory("orgId", &meraki.GetOrganizationWirelessDevicesSystemCpuLoadHistoryParams{
nResponse, _, err := client.Wireless.GetOrganizationWirelessDevicesSystemCPULoadHistory("orgId", &meraki.GetOrganizationWirelessDevicesSystemCPULoadHistoryQueryParams{
PerPage: 10,
Timespan: 300,
})
Expand All @@ -31,9 +31,9 @@ func main() {

for _, i := range *nResponse.Items {
fmt.Println("ID: ", i.Name)
if len(i.Series) > 0 {
fmt.Println("Time: ", i.Series[0].TS)
fmt.Println("CPU: ", i.Series[0].CPULoad5)
if i.Series != nil && len(*i.Series) > 0 {
fmt.Println("Time: ", (*i.Series)[0].Ts)
fmt.Println("CPU: ", (*i.Series)[0].CPULoad5)
} else {
fmt.Println("cpu data not available")
}
Expand Down
96 changes: 30 additions & 66 deletions sdk/administered.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,15 @@ func (s *AdministeredService) GetAdministeredIDentitiesMe() (*ResponseAdminister
path := "/api/v1/administered/identities/me"
s.rateLimiterBucket.Wait(1)

response, err := s.client.R().
SetHeader("Content-Type", "application/json").
SetHeader("Accept", "application/json").
SetResult(&ResponseAdministeredGetAdministeredIDentitiesMe{}).
SetError(&Error).
Get(path)
// Other way

if err != nil {
return nil, nil, err

}

if response.IsError() {
return nil, response, fmt.Errorf("error with operation GetAdministeredIdentitiesMe")
}

result := response.Result().(*ResponseAdministeredGetAdministeredIDentitiesMe)
return result, response, err
return doWithRetriesAndResult[ResponseAdministeredGetAdministeredIDentitiesMe](
func() (*resty.Response, error) {
return GET(path, s.client, &QueryParamsDefault, &HeaderDefault)
},
s.client,
nil,
)

}

Expand All @@ -85,24 +76,15 @@ func (s *AdministeredService) GetAdministeredIDentitiesMeAPIKeys() (*ResponseAdm
path := "/api/v1/administered/identities/me/api/keys"
s.rateLimiterBucket.Wait(1)

response, err := s.client.R().
SetHeader("Content-Type", "application/json").
SetHeader("Accept", "application/json").
SetResult(&ResponseAdministeredGetAdministeredIDentitiesMeAPIKeys{}).
SetError(&Error).
Get(path)

if err != nil {
return nil, nil, err

}

if response.IsError() {
return nil, response, fmt.Errorf("error with operation GetAdministeredIdentitiesMeApiKeys")
}
// Other way

result := response.Result().(*ResponseAdministeredGetAdministeredIDentitiesMeAPIKeys)
return result, response, err
return doWithRetriesAndResult[ResponseAdministeredGetAdministeredIDentitiesMeAPIKeys](
func() (*resty.Response, error) {
return GET(path, s.client, &QueryParamsDefault, &HeaderDefault)
},
s.client,
nil,
)

}

Expand All @@ -117,24 +99,15 @@ func (s *AdministeredService) GenerateAdministeredIDentitiesMeAPIKeys() (*Respon
path := "/api/v1/administered/identities/me/api/keys/generate"
s.rateLimiterBucket.Wait(1)

response, err := s.client.R().
SetHeader("Content-Type", "application/json").
SetHeader("Accept", "application/json").
SetResult(&ResponseAdministeredGenerateAdministeredIDentitiesMeAPIKeys{}).
SetError(&Error).
Post(path)
// Past way

if err != nil {
return nil, nil, err

}

if response.IsError() {
return nil, response, fmt.Errorf("error with operation GenerateAdministeredIdentitiesMeApiKeys")
}

result := response.Result().(*ResponseAdministeredGenerateAdministeredIDentitiesMeAPIKeys)
return result, response, err
return doWithRetriesAndResult[ResponseAdministeredGenerateAdministeredIDentitiesMeAPIKeys](
func() (*resty.Response, error) {
return POST(path, s.client, nil, nil)
},
s.client,
nil,
)

}

Expand All @@ -151,21 +124,12 @@ func (s *AdministeredService) RevokeAdministeredIDentitiesMeAPIKeys(suffix strin
s.rateLimiterBucket.Wait(1)
path = strings.Replace(path, "{suffix}", fmt.Sprintf("%v", suffix), -1)

response, err := s.client.R().
SetHeader("Content-Type", "application/json").
SetHeader("Accept", "application/json").
SetError(&Error).
Post(path)

if err != nil {
return nil, err

}

if response.IsError() {
return response, fmt.Errorf("error with operation RevokeAdministeredIdentitiesMeApiKeys")
}
// Past way

return response, err
return doWithRetriesAndNotResult(
func() (*resty.Response, error) {
return POST(path, s.client, nil, nil)
},
)

}
Loading