Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/unkeyed/unkey
Browse files Browse the repository at this point in the history
  • Loading branch information
chronark committed Sep 16, 2024
2 parents 5a967da + 4cac0e4 commit b725b84
Show file tree
Hide file tree
Showing 44 changed files with 525 additions and 151 deletions.
4 changes: 2 additions & 2 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
* @perkinsjr @chronark
/apps/www @domeccleston @perkinsjr @chronark @guilhermerodz
/apps/dashboard @perkinsjr @chronark @guilhermerodz
/apps/www @perkinsjr @chronark @MichaelUnkey @mcstepp
/apps/dashboard @perkinsjr @chronark @mcstepp

1 change: 0 additions & 1 deletion .github/workflows/unit_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ jobs:
- "./packages/cache"
- "./packages/hono"
- "./packages/nextjs"
- "./packages/nuxt"
- "./packages/rbac"

name: Test ${{matrix.path}}
Expand Down
19 changes: 17 additions & 2 deletions apps/agent/pkg/batch/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ import (
)

type BatchProcessor[T any] struct {
name string
drop bool
buffer chan T
batch []T
config Config[T]
flush func(ctx context.Context, batch []T)
}

type Config[T any] struct {
// drop events if the buffer is full
Drop bool
Name string
BatchSize int
BufferSize int
FlushInterval time.Duration
Expand All @@ -28,6 +33,8 @@ func New[T any](config Config[T]) *BatchProcessor[T] {
}

bp := &BatchProcessor[T]{
name: config.Name,
drop: config.Drop,
buffer: make(chan T, config.BufferSize),
batch: make([]T, 0, config.BatchSize),
flush: config.Flush,
Expand Down Expand Up @@ -71,15 +78,23 @@ func (bp *BatchProcessor[T]) process() {
flushAndReset()
}
}

}

func (bp *BatchProcessor[T]) Size() int {
return len(bp.buffer)
}

func (bp *BatchProcessor[T]) Buffer(t T) {
bp.buffer <- t
if bp.drop {

select {
case bp.buffer <- t:
default:
droppedMessages.WithLabelValues(bp.name).Inc()
}
} else {
bp.buffer <- t
}
}

func (bp *BatchProcessor[T]) Close() {
Expand Down
1 change: 0 additions & 1 deletion apps/agent/pkg/circuitbreaker/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ var (
var (
ErrTripped = errors.New("circuit breaker is open")
ErrTooManyRequests = errors.New("too many requests during half open state")
ErrTimeout = errors.New("circuit breaker timeout")
)

type CircuitBreaker[Res any] interface {
Expand Down
21 changes: 17 additions & 4 deletions apps/agent/pkg/circuitbreaker/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package circuitbreaker

import (
"context"
"fmt"
"sync"
"time"

"github.com/unkeyed/unkey/apps/agent/pkg/clock"
"github.com/unkeyed/unkey/apps/agent/pkg/logging"
"github.com/unkeyed/unkey/apps/agent/pkg/tracing"
)

type CB[Res any] struct {
Expand Down Expand Up @@ -139,22 +141,28 @@ func New[Res any](name string, applyConfigs ...applyConfig) *CB[Res] {
var _ CircuitBreaker[any] = &CB[any]{}

func (cb *CB[Res]) Do(ctx context.Context, fn func(context.Context) (Res, error)) (res Res, err error) {
ctx, span := tracing.Start(ctx, tracing.NewSpanName(fmt.Sprintf("circuitbreaker.%s", cb.config.name), "Do"))
defer span.End()

err = cb.preflight()
err = cb.preflight(ctx)
if err != nil {
return res, err
}

ctx, fnSpan := tracing.Start(ctx, tracing.NewSpanName(fmt.Sprintf("circuitbreaker.%s", cb.config.name), "fn"))
res, err = fn(ctx)
fnSpan.End()

cb.postflight(err)
cb.postflight(ctx, err)

return res, err

}

// preflight checks if the circuit is ready to accept a request
func (cb *CB[Res]) preflight() error {
func (cb *CB[Res]) preflight(ctx context.Context) error {
ctx, span := tracing.Start(ctx, tracing.NewSpanName(fmt.Sprintf("circuitbreaker.%s", cb.config.name), "preflight"))
defer span.End()
cb.Lock()
defer cb.Unlock()

Expand All @@ -174,9 +182,12 @@ func (cb *CB[Res]) preflight() error {
cb.resetStateAt = now.Add(cb.config.timeout)
}

requests.WithLabelValues(cb.config.name, string(cb.state)).Inc()

if cb.state == Open {
return ErrTripped
}

cb.logger.Info().Str("state", string(cb.state)).Int("requests", cb.requests).Int("maxRequests", cb.config.maxRequests).Msg("circuit breaker state")
if cb.state == HalfOpen && cb.requests >= cb.config.maxRequests {
return ErrTooManyRequests
Expand All @@ -185,7 +196,9 @@ func (cb *CB[Res]) preflight() error {
}

// postflight updates the circuit breaker state based on the result of the request
func (cb *CB[Res]) postflight(err error) {
func (cb *CB[Res]) postflight(ctx context.Context, err error) {
ctx, span := tracing.Start(ctx, tracing.NewSpanName(fmt.Sprintf("circuitbreaker.%s", cb.config.name), "postflight"))
defer span.End()
cb.Lock()
defer cb.Unlock()
cb.requests++
Expand Down
14 changes: 14 additions & 0 deletions apps/agent/pkg/circuitbreaker/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package circuitbreaker

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
requests = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "agent",
Subsystem: "circuitbreaker",
Name: "requests",
}, []string{"name", "state"})
)
22 changes: 18 additions & 4 deletions apps/agent/services/ratelimit/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import (
"sync"
"time"

"connectrpc.com/connect"

ratelimitv1 "github.com/unkeyed/unkey/apps/agent/gen/proto/ratelimit/v1"
"github.com/unkeyed/unkey/apps/agent/gen/proto/ratelimit/v1/ratelimitv1connect"
"github.com/unkeyed/unkey/apps/agent/pkg/circuitbreaker"
"github.com/unkeyed/unkey/apps/agent/pkg/cluster"
"github.com/unkeyed/unkey/apps/agent/pkg/logging"
"github.com/unkeyed/unkey/apps/agent/pkg/metrics"
Expand Down Expand Up @@ -33,6 +37,8 @@ type service struct {
leaseIdToKeyMapLock sync.RWMutex
// Store a reference leaseId -> window key
leaseIdToKeyMap map[string]string

syncCircuitBreaker circuitbreaker.CircuitBreaker[*connect.Response[ratelimitv1.PushPullResponse]]
}

type Config struct {
Expand All @@ -58,17 +64,25 @@ func New(cfg Config) (*service, error) {
buckets: make(map[string]*bucket),
leaseIdToKeyMapLock: sync.RWMutex{},
leaseIdToKeyMap: make(map[string]string),
syncCircuitBreaker: circuitbreaker.New[*connect.Response[ratelimitv1.PushPullResponse]](
"ratelimit.syncWithOrigin",
circuitbreaker.WithLogger(cfg.Logger),
circuitbreaker.WithCyclicPeriod(10*time.Second),
circuitbreaker.WithTimeout(time.Minute),
circuitbreaker.WithMaxRequests(100),
circuitbreaker.WithTripThreshold(50),
),
}

repeat.Every(time.Minute, s.removeExpiredIdentifiers)

if cfg.Cluster != nil {
s.mitigateBuffer = make(chan mitigateWindowRequest, 10000)
s.syncBuffer = make(chan syncWithOriginRequest, 10000)
s.mitigateBuffer = make(chan mitigateWindowRequest, 100000)
s.syncBuffer = make(chan syncWithOriginRequest, 100000)
// Process the individual requests to the origin and update local state
// We're using 32 goroutines to parallelise the network requests'
// We're using 128 goroutines to parallelise the network requests'
s.logger.Info().Msg("starting background jobs")
for range 32 {
for range 128 {
go func() {
for {
select {
Expand Down
6 changes: 5 additions & 1 deletion apps/agent/services/ratelimit/sync_with_origin.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ func (s *service) syncWithOrigin(req syncWithOriginRequest) {
return
}

res, err := client.PushPull(ctx, connect.NewRequest(req.req))
res, err := s.syncCircuitBreaker.Do(ctx, func(innerCtx context.Context) (*connect.Response[ratelimitv1.PushPullResponse], error) {
innerCtx, cancel = context.WithTimeout(innerCtx, 10*time.Second)
defer cancel()
return client.PushPull(innerCtx, connect.NewRequest(req.req))
})
if err != nil {
s.peersMu.Lock()
s.logger.Warn().Err(err).Msg("resetting peer client due to error")
Expand Down
2 changes: 0 additions & 2 deletions apps/api/src/routes/v1_keys_updateKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ This field will become required in a future version.`,
}),
}),
)
.min(1)
.optional()
.openapi({
description: `The roles you want to set for this key. This overwrites all existing roles.
Expand Down Expand Up @@ -210,7 +209,6 @@ This field will become required in a future version.`,
}),
}),
)
.min(1)
.optional()
.openapi({
description: `The permissions you want to set for this key. This overwrites all existing permissions.
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/routes/v1_keys_verifyKey.multilimit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ describe("without identities", () => {
},
});

console.info(res);
expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);
expect(res.body.valid).toBe(true);
expect(res.body.code).toBe("VALID");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async function AsyncPageBreadcrumb(props: PageProps) {
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{permissions.name}</BreadcrumbPage>
<BreadcrumbPage className="truncate w-96">{permissions.name}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const DeletePermission: React.FC<Props> = ({ trigger, permission }) => {
return (
<Dialog open={open} onOpenChange={(o) => setOpen(o)}>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent className="border-alert">
<DialogContent className="border-alert p-4 max-w-md mx-auto">
<DialogHeader>
<DialogTitle>Delete Permission</DialogTitle>
<DialogDescription>
Expand All @@ -96,10 +96,11 @@ export const DeletePermission: React.FC<Props> = ({ trigger, permission }) => {
<FormLabel className="font-normal text-content-subtle">
{" "}
Enter the permission's name{" "}
<span className="font-medium text-content">{permission.name}</span> to continue:
<span className="font-medium text-content break-all">{permission.name}</span> to
continue:
</FormLabel>
<FormControl>
<Input {...field} autoComplete="off" />
<Input {...field} autoComplete="off" className="w-full" />
</FormControl>

<FormMessage />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CopyButton } from "@/components/dashboard/copy-button";
import { PageHeader } from "@/components/dashboard/page-header";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getTenantId } from "@/lib/auth";
import { db } from "@/lib/db";
import {
Expand Down Expand Up @@ -69,6 +70,7 @@ export default async function RolesPage(props: Props) {
connectedKeys.add(key.keyId);
}
}
const shouldShowTooltip = permission.name.length > 16;

return (
<div className="flex flex-col min-h-screen gap-4">
Expand All @@ -79,10 +81,21 @@ export default async function RolesPage(props: Props) {
<Badge
key="permission-name"
variant="secondary"
className="flex justify-between w-full gap-2 font-mono font-medium ph-no-capture"
className="w-40 font-mono font-medium ph-no-capture"
>
{permission.name}
<CopyButton value={permission.name} />
<Tooltip>
<TooltipTrigger className="flex items-center justify-between gap-2 truncate">
<span className="truncate">{permission.name}</span>
<div>
<CopyButton value={permission.name} />
</div>
</TooltipTrigger>
{shouldShowTooltip && (
<TooltipContent>
<span className="text-xs font-medium">{permission.name}</span>
</TooltipContent>
)}
</Tooltip>
</Badge>,
<Badge
key="permission-id"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export default async function Layout({ children, params: { keyId } }: Props) {
</CardHeader>
<CardContent className="flex flex-wrap justify-between divide-x [&>div:first-child]:pl-0">
<Metric label="ID" value={<span className="font-mono">{key.id}</span>} />
<Metric label="Name" value={key.name ?? "-"} />
<Metric label="Created At" value={key.createdAt.toDateString()} />
<Metric
label={key.expires && key.expires.getTime() < Date.now() ? "Expired" : "Expires in"}
Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/app/(app)/settings/root-keys/[keyId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Api } from "./permissions/api";
import { Legacy } from "./permissions/legacy";
import { apiPermissions } from "./permissions/permissions";
import { Workspace } from "./permissions/workspace";
import { UpdateRootKeyName } from "./update-root-key-name";

export const dynamic = "force-dynamic";
export const runtime = "edge";
Expand Down Expand Up @@ -125,6 +126,8 @@ export default async function RootKeyPage(props: {
<Legacy keyId={key.id} permissions={permissions} />
) : null}

<UpdateRootKeyName apiKey={key} />

<Workspace keyId={key.id} permissions={permissions} />

{apisWithActivePermissions.map((api) => (
Expand Down
Loading

0 comments on commit b725b84

Please sign in to comment.