Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 31 additions & 12 deletions packages/api/internal/handlers/sandboxes_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"fmt"
"net/http"
"slices"
"sort"
"strconv"
"strings"
"time"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -90,10 +90,7 @@ func (a *APIStore) GetSandboxes(c *gin.Context, params api.GetSandboxesParams) {
runningSandboxes := getRunningSandboxes(sandboxes[instance.StateRunning], metadataFilter)

// Sort sandboxes by start time descending
slices.SortFunc(runningSandboxes, func(a, b utils.PaginatedSandbox) int {
// SortFunc sorts the list ascending by default, because we want the opposite behavior we switch `a` and `b`
return b.StartedAt.Compare(a.StartedAt)
})
sortPaginatedSandboxesDesc(runningSandboxes)

c.JSON(http.StatusOK, runningSandboxes)
}
Expand Down Expand Up @@ -149,6 +146,9 @@ func (a *APIStore) GetV2Sandboxes(c *gin.Context, params api.GetV2SandboxesParam

sandboxesInCache := a.orchestrator.GetSandboxes(ctx, &team.ID, []instance.State{instance.StateRunning, instance.StatePaused, instance.StatePausing})

// Sort sandboxes from the cache to properly apply the cursor filtering
sortCacheSandboxesDesc(sandboxesInCache)

// Running Sandbox IDs
runningSandboxesIDs := make([]string, 0)
for _, info := range sandboxesInCache[instance.StateRunning] {
Expand Down Expand Up @@ -187,13 +187,8 @@ func (a *APIStore) GetV2Sandboxes(c *gin.Context, params api.GetV2SandboxesParam
sandboxes = append(sandboxes, pausingSandboxList...)
}

// Sort by StartedAt (descending), then by SandboxID (ascending) for stability
sort.Slice(sandboxes, func(a, b int) bool {
if !sandboxes[a].StartedAt.Equal(sandboxes[b].StartedAt) {
return sandboxes[a].StartedAt.After(sandboxes[b].StartedAt)
}
return sandboxes[a].SandboxID < sandboxes[b].SandboxID
})
// We need to sort again after merging running and paused sandboxes
sortPaginatedSandboxesDesc(sandboxes)

var nextToken *string
if len(sandboxes) > int(limit) {
Expand Down Expand Up @@ -301,3 +296,27 @@ func instanceInfoToPaginatedSandboxes(runningSandboxes []*instance.InstanceInfo)

return sandboxes
}

// sortCacheSandboxesDesc sorts the sandboxes in the cache by StartedAt (descending),
// then by SandboxID (ascending) for stability
func sortCacheSandboxesDesc(cache map[instance.State][]*instance.InstanceInfo) {
for state := range cache {
slices.SortFunc(cache[state], func(a, b *instance.InstanceInfo) int {
if !a.StartTime.Equal(b.StartTime) {
return b.StartTime.Compare(a.StartTime)
}
return strings.Compare(a.SandboxID, b.SandboxID)
})
}
}

// sortPaginatedSandboxesDesc sorts the sandboxes by StartedAt (descending),
// then by SandboxID (ascending) for stability
func sortPaginatedSandboxesDesc(sandboxes []utils.PaginatedSandbox) {
slices.SortFunc(sandboxes, func(a, b utils.PaginatedSandbox) int {
if !a.StartedAt.Equal(b.StartedAt) {
return b.StartedAt.Compare(a.StartedAt)
}
return strings.Compare(a.SandboxID, b.SandboxID)
})
}
2 changes: 1 addition & 1 deletion packages/api/internal/utils/sandboxes_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func FilterBasedOnCursor(sandboxes []PaginatedSandbox, cursorTime time.Time, cur
}
sandboxes = filteredSandboxes

// Apply limit if provided (get limit + 1 for pagination if possible)
// Apply limit (get limit + 1 for pagination if possible)
if len(sandboxes) > int(limit) {
sandboxes = sandboxes[:limit+1]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ func TestSandboxListPaginationRunning(t *testing.T) {
sbx2 := utils.SetupSandboxWithCleanup(t, c, utils.WithMetadata(api.SandboxMetadata{metadataKey: metadataValue}))
sandbox2ID := sbx2.SandboxID

sbx3 := utils.SetupSandboxWithCleanup(t, c, utils.WithMetadata(api.SandboxMetadata{metadataKey: metadataValue}))
sandbox3ID := sbx3.SandboxID

// Test pagination with limit
var limit int32 = 1

Expand All @@ -254,12 +257,12 @@ func TestSandboxListPaginationRunning(t *testing.T) {
require.NoError(t, err)
require.Equal(t, http.StatusOK, listResponse.StatusCode())
require.Len(t, *listResponse.JSON200, 1)
assert.Equal(t, sandbox2ID, (*listResponse.JSON200)[0].SandboxID)
assert.Equal(t, sandbox3ID, (*listResponse.JSON200)[0].SandboxID)

totalHeader := listResponse.HTTPResponse.Header.Get("X-Total-Running")
total, err := strconv.Atoi(totalHeader)
require.NoError(t, err)
assert.Equal(t, 2, total)
assert.Equal(t, 3, total)

// Get second page using the next token from first response
nextToken := listResponse.HTTPResponse.Header.Get("X-Next-Token")
Expand All @@ -274,13 +277,93 @@ func TestSandboxListPaginationRunning(t *testing.T) {
require.NoError(t, err)
require.Equal(t, http.StatusOK, secondPageResponse.StatusCode())
require.Len(t, *secondPageResponse.JSON200, 1)
assert.Equal(t, sandbox1ID, (*secondPageResponse.JSON200)[0].SandboxID)
assert.Equal(t, sandbox2ID, (*secondPageResponse.JSON200)[0].SandboxID)

// No more pages
// Get third page using the next token from second response
nextToken = secondPageResponse.HTTPResponse.Header.Get("X-Next-Token")
assert.NotEmpty(t, nextToken)

thirdPageResponse, err := c.GetV2SandboxesWithResponse(t.Context(), &api.GetV2SandboxesParams{
Limit: &limit,
NextToken: &nextToken,
State: &[]api.SandboxState{api.Running},
Metadata: &metadataString,
}, setup.WithAPIKey())
require.NoError(t, err)
require.Equal(t, http.StatusOK, thirdPageResponse.StatusCode())
require.Len(t, *thirdPageResponse.JSON200, 1)
assert.Equal(t, sandbox1ID, (*thirdPageResponse.JSON200)[0].SandboxID)

// No more pages
nextToken = thirdPageResponse.HTTPResponse.Header.Get("X-Next-Token")
assert.Empty(t, nextToken)
}

func TestSandboxListPaginationRunningLargerLimit(t *testing.T) {
c := setup.GetAPIClient()

metadataKey := "uniqueIdentifier"
metadataValue := id.Generate()
metadataString := fmt.Sprintf("%s=%s", metadataKey, metadataValue)

sbxsCount := 12
sandboxes := make([]string, sbxsCount)
for i := range sbxsCount {
sbx := utils.SetupSandboxWithCleanup(t, c, utils.WithMetadata(api.SandboxMetadata{metadataKey: metadataValue}))
sandboxes[sbxsCount-i-1] = sbx.SandboxID

t.Logf("Created sandbox %d/%d: %s", i+1, sbxsCount, sbx.SandboxID)
}

t.Run("check all sandboxes list", func(t *testing.T) {
listResponse, err := c.GetV2SandboxesWithResponse(t.Context(), &api.GetV2SandboxesParams{
Limit: sharedUtils.ToPtr(int32(sbxsCount)),
State: &[]api.SandboxState{api.Running},
Metadata: &metadataString,
}, setup.WithAPIKey())
require.NoError(t, err)
require.Equal(t, http.StatusOK, listResponse.StatusCode())
require.Len(t, *listResponse.JSON200, sbxsCount)
for i := range sbxsCount {
assert.Equal(t, sandboxes[i], (*listResponse.JSON200)[i].SandboxID)
}
})

t.Run("check paginated list", func(t *testing.T) {
// Test pagination with limit
limit := 2

var nextToken *string
for i := 0; i < sbxsCount; i += limit {
sbxID := sandboxes[i]

listResponse, err := c.GetV2SandboxesWithResponse(t.Context(), &api.GetV2SandboxesParams{
Limit: sharedUtils.ToPtr(int32(limit)),
NextToken: nextToken,
State: &[]api.SandboxState{api.Running},
Metadata: &metadataString,
}, setup.WithAPIKey())
require.NoError(t, err)
require.Equal(t, http.StatusOK, listResponse.StatusCode())
require.Len(t, *listResponse.JSON200, int(limit))
assert.Equal(t, sbxID, (*listResponse.JSON200)[0].SandboxID, "page starting at %d should start with sandbox %s, token %s", i, sbxID, sharedUtils.Sprintp(nextToken))

totalHeader := listResponse.HTTPResponse.Header.Get("X-Total-Running")
total, err := strconv.Atoi(totalHeader)
require.NoError(t, err)
assert.Equal(t, sbxsCount, total)

nextToken = sharedUtils.ToPtr(listResponse.HTTPResponse.Header.Get("X-Next-Token"))

if i+limit == sbxsCount {
assert.Empty(t, *nextToken)
} else {
assert.NotEmpty(t, *nextToken)
}
}
})
}

func TestSandboxListPaginationPaused(t *testing.T) {
c := setup.GetAPIClient()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/e2b-dev/infra/packages/shared/pkg/utils"
"github.com/e2b-dev/infra/tests/integration/internal/api"
"github.com/e2b-dev/infra/tests/integration/internal/setup"
"github.com/e2b-dev/infra/tests/integration/internal/utils"
)

const (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/e2b-dev/infra/packages/shared/pkg/utils"
"github.com/e2b-dev/infra/tests/integration/internal/api"
"github.com/e2b-dev/infra/tests/integration/internal/setup"
"github.com/e2b-dev/infra/tests/integration/internal/utils"
)

func TestDeleteTemplate(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/e2b-dev/infra/packages/shared/pkg/utils"
"github.com/e2b-dev/infra/tests/integration/internal/api"
"github.com/e2b-dev/infra/tests/integration/internal/setup"
"github.com/e2b-dev/infra/tests/integration/internal/utils"
)

func TestRequestTemplateBuild(t *testing.T) {
Expand Down
5 changes: 0 additions & 5 deletions tests/integration/internal/utils/ptr.go

This file was deleted.

Loading