Azure DevOps 速率限制

huangapple go评论85阅读模式
英文:

Azure DevOps Rate Limit

问题

目标是使用Go语言检索Azure DevOps用户及其许可证和项目权限。

我正在使用Microsoft SDK

我们的Azure DevOps组织有超过1500个用户。因此,当我请求每个用户的权限时,由于Azure DevOps速率限制,我会收到一个错误消息,错误为443: read: connection reset by peer。

然而,限制每次请求的数量为100/200可以解决问题。

对于一个真正的解决方案,我考虑不再使用SDK,而是使用直接的REST API调用,并使用自定义的HTTP处理程序来支持速率限制。或者也可以考虑使用heimdall

你对一个好的设计有什么建议吗?

以下是代码:

package main

import (
	"context"
	"fmt"
	"github.com/microsoft/azure-devops-go-api/azuredevops"
	"github.com/microsoft/azure-devops-go-api/azuredevops/memberentitlementmanagement"
	"log"
	"runtime"
	"sync"
	"time"
)

var organizationUrl = "https://dev.azure.com/xxx"
var personalAccessToken = "xxx"

type User struct {
	DisplayName         string
	MailAddress         string
	PrincipalName       string
	LicenseDisplayName  string
	Status              string
	GroupAssignments    string
	ProjectEntitlements []string
	LastAccessedDate    azuredevops.Time
	DateCreated         azuredevops.Time
}

func init() {
	runtime.GOMAXPROCS(runtime.NumCPU()) // 尝试使用所有可用的CPU。
}

func main() {
	// 时间测量
	defer timeTrack(time.Now(), "Fetching Azure DevOps Users License and Projects")

	// 计算上下文
	fmt.Println("Version", runtime.Version())
	fmt.Println("NumCPU", runtime.NumCPU())
	fmt.Println("GOMAXPROCS", runtime.GOMAXPROCS(0))
	fmt.Println("Starting concurrent calls...")

	// 创建到组织的连接
	connection := azuredevops.NewPatConnection(organizationUrl, personalAccessToken)

	// 新建上下文
	ctx := context.Background()

	// 创建成员客户端
	memberClient, err := memberentitlementmanagement.NewClient(ctx, connection)
	if err != nil {
		log.Fatal(err)
	}

	// 请求所有用户
	top := 10000
	skip := 0
	filter := "Id"
	response, err := memberClient.GetUserEntitlements(ctx, memberentitlementmanagement.GetUserEntitlementsArgs{
		Top:        &top,
		Skip:       &skip,
		Filter:     &filter,
		SortOption: nil,
	})

	usersLen := len(*response.Members)

	allUsers := make(chan User, usersLen)

	var wg sync.WaitGroup
	wg.Add(usersLen)

	for _, user := range *response.Members {
		go func(user memberentitlementmanagement.UserEntitlement) {
			defer wg.Done()

			var userEntitlement = memberentitlementmanagement.GetUserEntitlementArgs{UserId: user.Id}
			account, err := memberClient.GetUserEntitlement(ctx, userEntitlement)
			if err != nil {
				log.Fatal(err)
			}

			var GroupAssignments string
			var ProjectEntitlements []string

			for _, assignment := range *account.GroupAssignments {
				GroupAssignments = *assignment.Group.DisplayName
			}

			for _, userProject := range *account.ProjectEntitlements {
				ProjectEntitlements = append(ProjectEntitlements, *userProject.ProjectRef.Name)
			}

			allUsers <- User{
				DisplayName:         *account.User.DisplayName,
				MailAddress:         *account.User.MailAddress,
				PrincipalName:       *account.User.PrincipalName,
				LicenseDisplayName:  *account.AccessLevel.LicenseDisplayName,
				DateCreated:         *account.DateCreated,
				LastAccessedDate:    *account.LastAccessedDate,
				GroupAssignments:    GroupAssignments,
				ProjectEntitlements: ProjectEntitlements,
			}
		}(user)
	}

	wg.Wait()
	close(allUsers)
	for eachUser := range allUsers {
		fmt.Println(eachUser)
	}
}

func timeTrack(start time.Time, name string) {
	elapsed := time.Since(start)
	log.Printf("%s took %s", name, elapsed)
}

希望对你有所帮助!

英文:

Goal is to retrieve Azure DevOps users with their license and project entitlements in go.

I'm using Microsoft SDK.

Our Azure DevOps organization has more than 1500 users. So when I request each user entitlements, I have an error message due to Azure DevOps rate limit => 443: read: connection reset by peer

However, limiting top with 100/200 does the job, of course..

For a real solution, I though not using SDK anymore and using direct REST API calls with a custom http handler which would support rate limit. Or maybe using heimdall.

What is your advise for a good design guys ?

Thanks.

Here is code :

package main
import (
&quot;context&quot;
&quot;fmt&quot;
&quot;github.com/microsoft/azure-devops-go-api/azuredevops&quot;
&quot;github.com/microsoft/azure-devops-go-api/azuredevops/memberentitlementmanagement&quot;
&quot;log&quot;
&quot;runtime&quot;
&quot;sync&quot;
&quot;time&quot;
)
var organizationUrl = &quot;https://dev.azure.com/xxx&quot;
var personalAccessToken = &quot;xxx&quot;
type User struct {
DisplayName         string
MailAddress         string
PrincipalName       string
LicenseDisplayName  string
Status              string
GroupAssignments    string
ProjectEntitlements []string
LastAccessedDate    azuredevops.Time
DateCreated         azuredevops.Time
}
func init() {
runtime.GOMAXPROCS(runtime.NumCPU()) // Try to use all available CPUs.
}
func main() {
// Time measure
defer timeTrack(time.Now(), &quot;Fetching Azure DevOps Users License and Projects&quot;)
// Compute context
fmt.Println(&quot;Version&quot;, runtime.Version())
fmt.Println(&quot;NumCPU&quot;, runtime.NumCPU())
fmt.Println(&quot;GOMAXPROCS&quot;, runtime.GOMAXPROCS(0))
fmt.Println(&quot;Starting concurrent calls...&quot;)
// Create a connection to your organization
connection := azuredevops.NewPatConnection(organizationUrl, personalAccessToken)
// New context
ctx := context.Background()
// Create a member client
memberClient, err := memberentitlementmanagement.NewClient(ctx, connection)
if err != nil {
log.Fatal(err)
}
// Request all users
top := 10000
skip := 0
filter := &quot;Id&quot;
response, err := memberClient.GetUserEntitlements(ctx, memberentitlementmanagement.GetUserEntitlementsArgs{
Top:        &amp;top,
Skip:       &amp;skip,
Filter:     &amp;filter,
SortOption: nil,
})
usersLen := len(*response.Members)
allUsers := make(chan User, usersLen)
var wg sync.WaitGroup
wg.Add(usersLen)
for _, user := range *response.Members {
go func(user memberentitlementmanagement.UserEntitlement) {
defer wg.Done()
var userEntitlement = memberentitlementmanagement.GetUserEntitlementArgs{UserId: user.Id}
account, err := memberClient.GetUserEntitlement(ctx, userEntitlement)
if err != nil {
log.Fatal(err)
}
var GroupAssignments string
var ProjectEntitlements []string
for _, assignment := range *account.GroupAssignments {
GroupAssignments = *assignment.Group.DisplayName
}
for _, userProject := range *account.ProjectEntitlements {
ProjectEntitlements = append(ProjectEntitlements, *userProject.ProjectRef.Name)
}
allUsers &lt;- User{
DisplayName:         *account.User.DisplayName,
MailAddress:         *account.User.MailAddress,
PrincipalName:       *account.User.PrincipalName,
LicenseDisplayName:  *account.AccessLevel.LicenseDisplayName,
DateCreated:         *account.DateCreated,
LastAccessedDate:    *account.LastAccessedDate,
GroupAssignments:    GroupAssignments,
ProjectEntitlements: ProjectEntitlements,
}
}(user)
}
wg.Wait()
close(allUsers)
for eachUser := range allUsers {
fmt.Println(eachUser)
}
}
func timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
log.Printf(&quot;%s took %s&quot;, name, elapsed)
}

答案1

得分: 1

你可以编写自定义版本的GetUserEntitlement函数。

https://github.com/microsoft/azure-devops-go-api/blob/dev/azuredevops/memberentitlementmanagement/client.go#L297-L314

它不使用任何私有成员。

在获取http.Response后,你可以检查Retry-After头部,如果存在的话,延迟下一次循环的迭代。

https://github.com/microsoft/azure-devops-go-api/blob/dev/azuredevops/memberentitlementmanagement/client.go#L306

附注:你的代码中的并发是多余的,可以移除。

更新 - 解释并发问题:

在并发代码中,你不能轻松地实现速率限制。如果你按顺序执行所有请求,并在每个响应之前检查Retry-After头部,那么会简单得多。

在并行执行中:1)你不能依赖Retry-After头部的值,因为你可能同时有另一个请求正在执行并返回不同的值。2)你不能对其他请求应用延迟,因为其中一些请求已经在进行中。

英文:

You can write custom version of GetUserEntitlement function.

https://github.com/microsoft/azure-devops-go-api/blob/dev/azuredevops/memberentitlementmanagement/client.go#L297-L314

It does not use any private members.

After getting http.Response you can check Retry-After header and delay next loop's iteration if it is present.

https://github.com/microsoft/azure-devops-go-api/blob/dev/azuredevops/memberentitlementmanagement/client.go#L306

P.S. Concurrency in your code is redundant and can be removed.

Update - explaining concurrency issue:

You cannot easily implement rate-limiting in concurrent code. It will be much simpler if you execute all requests sequentially and check Retry-After header in every response before moving to the next one.

With parallel execution: 1) you cannot rely on Retry-After header value because you may have another request executing at the same time returning a different value. 2) You cannot apply delay to other requests because some of them are already in progress.

答案2

得分: 0

你的意思是你想通过直接使用REST API来避免速率限制吗?

如果是这样,那么你的想法是行不通的。

大多数REST API都可以通过客户端库进行访问,如果你使用基于REST API的SDK或其他基于REST API的工具,它当然会受到速率限制的影响。

由于速率限制是基于用户的,我建议你可以基于多个用户完成你的操作(前提是你的请求不会太频繁以至于服务器封锁你的IP)。

英文:

> For a real solution, I though not using SDK anymore and using direct
> REST API calls with a custom http handler which would support rate
> limit. Or maybe using heimdall.

Do you mean you want to avoid the Rate Limit by using the REST API directly?

If so, then your idea will not work.

Most REST APIs are accessible through client libraries, and if you're using SDK based on a REST API or other thing based on a REST API, it will of course hit a rate limit.

Since the rate limit is based on users, I suggest that you can complete your operations based on multiple users (provided that your request is not too much that the server blocking your IP).

huangapple
  • 本文由 发表于 2022年6月6日 20:22:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/72517644.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定