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
18 changes: 18 additions & 0 deletions entities/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "entities",
srcs = ["request.go"],
importpath = "github.com/uber/submitqueue/entities",
visibility = ["//visibility:public"],
)

go_test(
name = "entities_test",
srcs = ["request_test.go"],
embed = [":entities"],
deps = [
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
)
97 changes: 97 additions & 0 deletions entities/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package entities

import (
"fmt"
"strconv"
"strings"
)

// RequestLandStrategy defines the possible source control integration methods.
type RequestLandStrategy int

const (
// RequestLandStrategyDefault lets the server decide based on configuration.
RequestLandStrategyDefault RequestLandStrategy = 0
// RequestLandStrategyRebase rebases commits onto the target branch before landing.
RequestLandStrategyRebase = 1
// RequestLandStrategySquashRebase squashes commits into a single commit before rebase.
RequestLandStrategySquashRebase = 2
// RequestLandStrategyMerge merges commits into the target branch by creating a separate merge commit, preserving the commit history along with hashes.
RequestLandStrategyMerge = 3
)

type RequestState int

// TODO: define all states
const (
// RequestStateUnknown is the unreachable state. It is set by default when the structure is initialized. It should never be seen in the system.
RequestStateUnknown RequestState = 0
// RequestStateNew is the initial state of a land request. It is confirmed by the system but the processing is not started yet.
RequestStateNew RequestState = 1
// RequestStateProcessing is the state of a land request that is being processed.
RequestStateProcessing = 2
// RequestStateLanded is the state of a land request that has been successfully processed and landed. This is the final state.
RequestStateLanded = 3
// RequestStateError is the state of a land request that has encountered an error. This is the final state.
RequestStateError = 4
)

// Change represents a set of related code changes identified by one or more IDs from a particular code change provider, like Github Pull Requests.
// The object is immutable after creation.
type Change struct {
// Source is the code change provider (e.g., "github", "gerrit", "phabricator").
Source string
// IDs is a list of change IDs, in a format specific to the code change provider, that should be landed together.
IDs []string
}

// Request defines a request to land (merge into target branch of the source control repository) a set of code changes.
// The object is immutable after creation.
type Request struct {
Comment thread
behinddwalls marked this conversation as resolved.
// ****************
// Immutable fields, fixed at request entity creation
// ****************

// Queue is the name of the queue processing the land request. Queue name is defined in the configuration and should be unique within the system.
Queue string
// Seq is an autoincrementing integer identifier for the land request. It is unique within the queue.
Seq int64
// Change is a number of code changes (such as pull requests) to land into the target branch. Target branch is defined by the queue configuration.
Change Change
// LandStrategy is the source control integration strategy to use for this land operation. If not specified, the default queue strategy is used.
LandStrategy RequestLandStrategy

// ****************
// Following fields could be changed throughout the lifecycle of the request
// ****************

// State is the current state of the land request.
State RequestState
// Version is the version of the object. It is used for optimistic locking.
// Versioning starts at 1 and is incremented for each change to the object.
Version int32
}

// GetID returns the globally unique identifier for the land request.
func (r *Request) GetID() string {
return fmt.Sprintf("%s/%d", r.Queue, r.Seq)
}

// ParseRequestID parses the globally unique identifier for the land request and returns the queue name and sequence number.
func ParseRequestID(id string) (queue string, seq int64, err error) {
parts := strings.Split(id, "/")
if len(parts) != 2 {
return "", 0, fmt.Errorf("invalid format of the request ID: %s; expected format: <queue>/<seq>", id)
}

seq, err = strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return "", 0, fmt.Errorf("invalid sequence number in the request ID: %s; expected format: <queue>/<seq>; parsing error: %w", id, err)
}

if seq <= 0 {
return "", 0, fmt.Errorf("invalid sequence number in the request ID: %s; expected format: <queue>/<seq>; sequence number must be greater than 0 but got %d", id, seq)
}

return parts[0], seq, nil
}
112 changes: 112 additions & 0 deletions entities/request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package entities

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRequest_GetID(t *testing.T) {
tests := []struct {
name string
request Request
expected string
}{
{
name: "standard ID",
request: Request{Queue: "my-queue", Seq: 42},
expected: "my-queue/42",
},
{
name: "seq 1",
request: Request{Queue: "q", Seq: 1},
expected: "q/1",
},
{
name: "large seq",
request: Request{Queue: "prod", Seq: 9999999},
expected: "prod/9999999",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.request.GetID())
})
}
}

func TestParseRequestID(t *testing.T) {
tests := []struct {
name string
id string
wantQueue string
wantSeq int64
expectError bool
}{
{
name: "valid ID",
id: "my-queue/42",
wantQueue: "my-queue",
wantSeq: 42,
},
{
name: "seq 1",
id: "q/1",
wantQueue: "q",
wantSeq: 1,
},
{
name: "missing separator",
id: "no-separator",
expectError: true,
},
{
name: "too many separators",
id: "a/b/c",
expectError: true,
},
{
name: "empty string",
id: "",
expectError: true,
},
{
name: "non-numeric seq",
id: "queue/abc",
expectError: true,
},
{
name: "zero seq",
id: "queue/0",
expectError: true,
},
{
name: "negative seq",
id: "queue/-1",
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
queue, seq, err := ParseRequestID(tt.id)
if tt.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantQueue, queue)
assert.Equal(t, tt.wantSeq, seq)
})
}
}

func TestGetID_ParseRequestID_Roundtrip(t *testing.T) {
req := &Request{Queue: "test-queue", Seq: 123}
queue, seq, err := ParseRequestID(req.GetID())
require.NoError(t, err)
assert.Equal(t, req.Queue, queue)
assert.Equal(t, req.Seq, seq)
}
8 changes: 0 additions & 8 deletions entities/storage/BUILD.bazel

This file was deleted.

5 changes: 0 additions & 5 deletions entities/storage/land_request.go

This file was deleted.

4 changes: 2 additions & 2 deletions extensions/storage/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "storage",
srcs = [
"factory.go",
"storage.go",
"request_store.go",
],
importpath = "github.com/uber/submitqueue/extensions/storage",
visibility = ["//visibility:public"],
deps = ["//entities/storage"],
deps = ["//entities"],
)
6 changes: 0 additions & 6 deletions extensions/storage/factory.go

This file was deleted.

2 changes: 1 addition & 1 deletion extensions/storage/mysql/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ go_library(
importpath = "github.com/uber/submitqueue/extensions/storage/mysql",
visibility = ["//visibility:public"],
deps = [
"//entities/storage",
"//entities",
"//extensions/storage",
],
)
20 changes: 10 additions & 10 deletions extensions/storage/mysql/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ package mysql

import "github.com/uber/submitqueue/extensions/storage"

type factory struct {
requestStore storage.RequestStore
}

type FactoryParams struct {
requestStore storage.RequestStore
// MySQLParameters defines the parameters for the MySQL storage factory.
type MySQLParameters struct {
}

// NewFactory creates a new MySQL storage factory
func NewFactory(p FactoryParams) (storage.Factory, error) {
func NewFactory(p MySQLParameters) (storage.StoreFactory, error) {
return &factory{
requestStore: p.requestStore,
requestStore: NewRequestStore(),
}, nil
}

// RequestStore returns the MySQL-backed RequestStore
func (f *factory) RequestStore() storage.RequestStore {
type factory struct {
requestStore storage.RequestStore
}

// GetRequestStore returns the MySQL-backed RequestStore
func (f *factory) GetRequestStore() storage.RequestStore {
return f.requestStore
}
26 changes: 19 additions & 7 deletions extensions/storage/mysql/request_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,35 @@ package mysql

import (
"context"
"errors"

entities "github.com/uber/submitqueue/entities/storage"
"github.com/uber/submitqueue/entities"
"github.com/uber/submitqueue/extensions/storage"
)

type requestStore struct {
}

type RequestParam struct {
}

// NewRequestStore creates a new MySQL-backed RequestStore.
func NewRequestStore() storage.RequestStore {
return &requestStore{}
}

// Get retrieves a land request by ID
func (r *requestStore) Get(ctx context.Context, id string) (*entities.LandRequest, error) {
// Get retrieves a land request by ID. Returns ErrNotFound if the request is not found.
func (r *requestStore) Get(ctx context.Context, id string) (entities.Request, error) {
// TODO: implement GET operation
panic("not implemented")
return entities.Request{}, errors.New("not implemented")
}

// Create creates a new land request. Returns the created request object with generated sequence number.
func (r *requestStore) Create(ctx context.Context, queue string, change entities.Change, strategy entities.RequestLandStrategy, state entities.RequestState) (entities.Request, error) {
// TODO: implement CREATE operation
return entities.Request{}, errors.New("not implemented")
}

// UpdateState updates the state of a land request if the current version matches the expected version. If versions do not match, returns ErrVersionMismatch.
// The implementation should increment the version by 1 atomically with the state update.
func (r *requestStore) UpdateState(ctx context.Context, id string, version int32, newState entities.RequestState) error {
// TODO: implement UPDATE STATE operation
return errors.New("not implemented")
}
16 changes: 12 additions & 4 deletions extensions/storage/request_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ package storage
import (
"context"

"github.com/uber/submitqueue/entities/storage"
"github.com/uber/submitqueue/entities"
)

// RequestStore is an interface that defines methods for managing storage requests.
// RequestStore is an interface that defines methods for managing land requests in the database.
type RequestStore interface {
// Get retrieves a land request by ID
Get(ctx context.Context, id string) (*storage.LandRequest, error)
// Get retrieves a land request by ID (queue/seq). Returns ErrNotFound if the request is not found.
Get(ctx context.Context, id string) (entities.Request, error)

// Create creates a new land request. Returns the created request object with generated sequence number.
// The implementation must ensure that the sequence number is unique within the queue.
Create(ctx context.Context, queue string, change entities.Change, strategy entities.RequestLandStrategy, state entities.RequestState) (entities.Request, error)

// UpdateState updates the state of a land request if the current version matches the expected version. If versions do not match, returns ErrVersionMismatch.
// The implementation should increment the version by 1 atomically with the state update.
UpdateState(ctx context.Context, id string, version int32, newState entities.RequestState) error
}
18 changes: 18 additions & 0 deletions extensions/storage/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package storage

import "errors"

// ErrNotFound is returned by storage implementations when the requested record is not found in the database.
var ErrNotFound = errors.New("record not found")

// ErrVersionMismatch is returned by storage implementations when the expected entity version does not match the current version of the object.
// This is used to implement an optimistic locking mechanism, allowing multiple clients to update the same entity concurrently
// and either retry or implement idempotent operations.
var ErrVersionMismatch = errors.New("version mismatch")

// StoreFactory is an interface that defines methods for creating different stores..
// Each store is responsible for performing atomic storage operations for a specific entity type.
type StoreFactory interface {
// GetRequestStore creates a new RequestStore instance.
GetRequestStore() RequestStore
}