go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/examples/appengine/quotabeta/rpc/rpc.go (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package rpc implements a rate-limited RPC service.
    16  package rpc
    17  
    18  import (
    19  	"context"
    20  
    21  	"google.golang.org/protobuf/runtime/protoiface"
    22  
    23  	"google.golang.org/grpc/codes"
    24  	"google.golang.org/protobuf/types/known/emptypb"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/common/logging"
    28  	"go.chromium.org/luci/grpc/appstatus"
    29  	"go.chromium.org/luci/server/auth"
    30  	"go.chromium.org/luci/server/quotabeta"
    31  
    32  	pb "go.chromium.org/luci/examples/appengine/quotabeta/proto"
    33  )
    34  
    35  // Demo implements pb.DemoServer. Requires a quotaconfig.Interface in the
    36  // context for all method calls.
    37  type Demo struct {
    38  }
    39  
    40  // Ensure Demo implements pb.DemoServer at compile-time.
    41  var _ pb.DemoServer = &Demo{}
    42  
    43  // GlobalRateLimit is globally limited to one request every 60 seconds. This
    44  // quota can be reset at any time by calling GlobalQuotaReset. On success,
    45  // returns an *emptypb.Empty, and on failure returns a codes.ResourceExhausted
    46  // gRPC error.
    47  func (*Demo) GlobalRateLimit(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
    48  	// global-rate-limit has a maximum of 60 resources. When below 60,
    49  	// automatically replenishes one resource per second. By debiting
    50  	// 60 resources on every call, enforce a rate limit of one request
    51  	// every 60 seconds.
    52  	updates := map[string]int64{
    53  		"global-rate-limit": -60,
    54  	}
    55  	switch err := quota.UpdateQuota(ctx, updates, nil); err {
    56  	case nil:
    57  		return &emptypb.Empty{}, nil
    58  	case quota.ErrInsufficientQuota:
    59  		return nil, appstatus.Errorf(codes.ResourceExhausted, "global rate limit exceeded")
    60  	default:
    61  		return nil, errors.Annotate(err, "quota.UpdateQuota").Err()
    62  	}
    63  }
    64  
    65  // GlobalQuotaReset resets quota for calling GlobalRateLimit. Always succeeds,
    66  // returning an *emptypb.Empty.
    67  func (*Demo) GlobalQuotaReset(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
    68  	// global-rate-limit has a maximum of 60 resources. Credit all 60.
    69  	updates := map[string]int64{
    70  		"global-rate-limit": 60,
    71  	}
    72  	switch err := quota.UpdateQuota(ctx, updates, nil); err {
    73  	case nil:
    74  		return &emptypb.Empty{}, nil
    75  	default:
    76  		return nil, errors.Annotate(err, "quota.UpdateQuota").Err()
    77  	}
    78  }
    79  
    80  // PerUserRateLimit is limited to two requests every 60 seconds for a given user.
    81  // This quota can be reset at any time by calling PerUserQuotaReset. On success,
    82  // returns an *emptypb.Empty, and on failure returns a codes.ResourceExhausted
    83  // gRPC error.
    84  func (*Demo) PerUserRateLimit(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
    85  	// per-user-rate-limit/${user} has a maximum of 60 resources per user.
    86  	// When below 60, automatically replenishes one resource per second. By
    87  	// debiting 30 resources on every call, enforce a rate limit of two requests
    88  	// every 60 seconds.
    89  	updates := map[string]int64{
    90  		"per-user-rate-limit/${user}": -30,
    91  	}
    92  	opts := &quota.Options{
    93  		User: string(auth.CurrentIdentity(ctx)),
    94  	}
    95  	switch err := quota.UpdateQuota(ctx, updates, opts); err {
    96  	case nil:
    97  		return &emptypb.Empty{}, nil
    98  	case quota.ErrInsufficientQuota:
    99  		return nil, appstatus.Errorf(codes.ResourceExhausted, "per-user rate limit exceeded")
   100  	default:
   101  		return nil, errors.Annotate(err, "quota.UpdateQuota").Err()
   102  	}
   103  }
   104  
   105  // PerUserQuotaReset resets the caller's own quota for calling PerUserRateLimit.
   106  // Always succeeds, returning an *emptypb.Empty.
   107  func (*Demo) PerUserQuotaReset(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
   108  	// per-user-rate-limit has a maximum of 60 resources per user. Credit all 60.
   109  	updates := map[string]int64{
   110  		"per-user-rate-limit/${user}": 60,
   111  	}
   112  	opts := &quota.Options{
   113  		User: string(auth.CurrentIdentity(ctx)),
   114  	}
   115  	switch err := quota.UpdateQuota(ctx, updates, opts); err {
   116  	case nil:
   117  		return &emptypb.Empty{}, nil
   118  	default:
   119  		return nil, errors.Annotate(err, "quota.UpdateQuota").Err()
   120  	}
   121  }
   122  
   123  // New returns a new pb.DemoServer.
   124  func New() pb.DemoServer {
   125  	return &pb.DecoratedDemo{
   126  		// Prelude logs the details of every request.
   127  		Prelude: func(ctx context.Context, methodName string, _ protoiface.MessageV1) (context.Context, error) {
   128  			logging.Debugf(ctx, "%q called %q", auth.CurrentIdentity(ctx), methodName)
   129  			return ctx, nil
   130  		},
   131  
   132  		Service: &Demo{},
   133  
   134  		// Postlude logs non-GRPC errors, and returns them as gRPC internal errors.
   135  		Postlude: func(ctx context.Context, _ string, _ protoiface.MessageV1, err error) error {
   136  			return appstatus.GRPCifyAndLog(ctx, err)
   137  		},
   138  	}
   139  }