Documentation
¶
Overview ¶
Package flexi implements an in-memory matchmaking engine compatible with Amazon GameLift FlexMatch rule sets.
The engine accepts the AWS FlexMatch rule set JSON document — the same payload passed to CreateMatchmakingRuleSet's RuleSetBody parameter — and evaluates matchmaking tickets against it, producing matches whose teams satisfy every configured rule.
Scope ¶
flexi targets FlexMatch's "standalone" use case: pure rule evaluation with no GameLift hosting integration, no networking, and no persistence. The ticket queue is held in memory only.
The rule set must declare ruleLanguageVersion "1.0" (the only version AWS FlexMatch supports); a missing or different value is rejected.
Supported rule set features:
- Player attribute types: string, number, string_list, string_number_map, with default values applied to players that omit an attribute. A value whose kind disagrees with the declared type is rejected at Matchmaker.Enqueue; attributes not declared in the rule set are carried through unchecked.
- Property expressions in the AWS dialect, e.g. teams[red].players.attributes[skill]; aggregations min, max, avg, median, sum, count, stddev, flatten, set_intersection, with per-team nesting for multi-team scopes (teams[a,b], teams[*]).
- Algorithm strategies: exhaustiveSearch, balanced (with balancedAttribute).
- Algorithm batchingPreference (random, sorted, largestPopulation, fastestRegion), sortByAttributes, backfillPriority, expansionAgeSelection.
- Teams with minPlayers, maxPlayers, and quantity (multi-instance teams).
- All eight FlexMatch rule kinds: comparison, distance, absoluteSort, distanceSort, batchDistance, collection, latency, compound (with a statement string using and/or/not/xor).
- partyAggregation (min/max/avg, or union/intersection for collection) for multi-player tickets.
A few rule types follow the AWS semantics precisely enough to be worth spelling out:
- collection: "contains" counts how many times the reference value occurs in the measurement (bounded by minCount/maxCount); "intersection" counts the values shared by every player's collection and takes no referenceValue; "reference_intersection_count" requires each player's collection to intersect the reference value within minCount/maxCount.
- batchDistance: a numeric attribute is grouped by spread (maxDistance); a string attribute is grouped by value equivalency, and with no maxDistance it requires every player to share one value.
- maxDistance / minDistance accept either a JSON number or a string-encoded number (e.g. "500"), matching the inconsistent AWS documentation.
- Time-driven expansions that loosen rule values, team sizes, or algorithm fields once a ticket has been waiting long enough.
- Rule evaluation metrics (FlexMatch's ruleEvaluationMetrics): per-rule pass/fail tallies on each Match and Proposal, plus cumulative per-ticket totals via Matchmaker.RuleMetrics.
Backfill of in-progress matches is intentionally out of scope; the backfillPriority field is validated but does not change matching behaviour.
Quick start ¶
mm, err := flexi.New(rulesetJSON)
if err != nil { ... }
mm.Enqueue(flexi.Ticket{
ID: "ticket-1",
Players: []flexi.Player{{
ID: "alice",
Attributes: flexi.Attributes{"skill": flexi.Number(1500)},
Latencies: map[string]int{"us-east-1": 35},
}},
})
matches, err := mm.Tick()
for _, m := range matches {
// m.Teams maps team name -> assigned players
// m.TicketIDs lists tickets consumed by the match
}
Driving the matchmaker ¶
Matchmaker has no internal goroutines or timers. Callers drive it by invoking Matchmaker.Tick, which returns every match that can be formed against the current queue. This keeps tests deterministic and lets callers integrate with whatever scheduling, observability, or shutdown story they already have. A typical production loop looks like:
t := time.NewTicker(time.Second)
for range t.C {
matches, err := mm.Tick()
// dispatch matches, log err
}
Time and expansions ¶
Anything that depends on elapsed time (most importantly the FlexMatch "expansions" block) reads the current time through a Clock. The default is SystemClock; tests should pass WithClock with a FakeClock so they can advance time deterministically without sleeping.
Concurrency ¶
All Matchmaker methods are safe for concurrent use. The queue is protected by an internal mutex, so producers may Enqueue/Cancel from any goroutine while another goroutine drives Tick. Tick itself is intended to be called from a single goroutine — concurrent ticks are safe but compete for the same ticket pool.
Index ¶
- Constants
- Variables
- type Attribute
- type AttributeKind
- type Attributes
- type Clock
- type FakeClock
- type Match
- type Matchmaker
- func (m *Matchmaker) Accept(ticketID, playerID string) error
- func (m *Matchmaker) Cancel(ticketID string) error
- func (m *Matchmaker) Enqueue(t Ticket) error
- func (m *Matchmaker) MarkCompleted(ticketID string) error
- func (m *Matchmaker) Pending() int
- func (m *Matchmaker) PendingAcceptances() []Proposal
- func (m *Matchmaker) Reject(ticketID, playerID string) error
- func (m *Matchmaker) RuleMetrics(ticketID string) ([]RuleMetric, bool)
- func (m *Matchmaker) Status(ticketID string) (TicketStatus, error)
- func (m *Matchmaker) StatusReason(ticketID string) (StatusReason, bool)
- func (m *Matchmaker) Tick() ([]Match, error)
- type Option
- type Player
- type Proposal
- type RuleMetric
- type StatusReason
- type SystemClock
- type Ticket
- type TicketStatus
Constants ¶
const ( // AttrUnknown is the zero value and should not appear in well-formed input. AttrUnknown = core.AttrUnknown // AttrString corresponds to FlexMatch's "string" attribute type. AttrString = core.AttrString // AttrNumber corresponds to FlexMatch's "number" attribute type. AttrNumber = core.AttrNumber // AttrStringList corresponds to FlexMatch's "string_list" attribute type. AttrStringList = core.AttrStringList // AttrStringNumberMap corresponds to FlexMatch's "string_number_map" type. AttrStringNumberMap = core.AttrStringNumberMap )
AttributeKind constants. Use String, Number, StringList, or StringNumberMap to construct attributes rather than setting Kind by hand.
const ( StatusQueued = core.StatusQueued StatusSearching = core.StatusSearching StatusRequiresAcceptance = core.StatusRequiresAcceptance StatusPlacing = core.StatusPlacing StatusCompleted = core.StatusCompleted StatusFailed = core.StatusFailed StatusCancelled = core.StatusCancelled StatusTimedOut = core.StatusTimedOut )
const ( // StatusReasonAcceptanceFailed is set on a ticket that has returned to // [StatusSearching] because a proposed match it had accepted failed to // gather every required acceptance (a sibling player rejected or timed // out). It corresponds to FlexMatch's MatchmakingSearching event being // re-emitted with a status reason after a proposed match fails. StatusReasonAcceptanceFailed = core.StatusReasonAcceptanceFailed )
Variables ¶
var ErrDuplicateTicket = queue.ErrDuplicateTicket
ErrDuplicateTicket is returned by Matchmaker.Enqueue when a ticket with the same ID is already in the queue.
var ErrInvalidRuleSet = ruleset.ErrInvalidRuleSet
ErrInvalidRuleSet is returned by New when the rule set JSON is malformed or fails semantic validation. It is suitable for use with errors.Is:
if errors.Is(err, flexi.ErrInvalidRuleSet) { ... }
var ErrTicketNotPlacing = errors.New("flexi: ticket is not in PLACING")
ErrTicketNotPlacing is returned by MarkCompleted when the ticket is not in StatusPlacing.
var ErrUnknownPlayer = errors.New("flexi: player is not part of ticket")
ErrUnknownPlayer is returned by Accept / Reject when the player id is not part of the referenced ticket.
var ErrUnknownProposal = errors.New("flexi: ticket is not in a pending proposal")
ErrUnknownProposal is returned by Accept / Reject when the ticket is not part of any active proposal (either it was never proposed, or the proposal has already been resolved).
var ErrUnknownTicket = queue.ErrUnknownTicket
ErrUnknownTicket is returned by Matchmaker.Cancel or Matchmaker.Status when no ticket with the given ID is currently tracked.
Functions ¶
This section is empty.
Types ¶
type Attribute ¶
Attribute is a tagged union mirroring a single FlexMatch player attribute value. Only the field selected by Kind is meaningful; other fields are zero.
Construct attributes with String, Number, StringList, or StringNumberMap rather than building the struct literally.
func Number ¶
Number returns an Attribute of kind AttrNumber holding v.
func String ¶
String returns an Attribute of kind AttrString holding v.
func StringList ¶
StringList returns an Attribute of kind AttrStringList holding a copy of v. Subsequent mutations of v will not affect the returned attribute.
func StringNumberMap ¶
StringNumberMap returns an Attribute of kind AttrStringNumberMap holding a copy of v. Subsequent mutations of v will not affect the returned attribute.
type AttributeKind ¶
type AttributeKind = core.AttributeKind
AttributeKind identifies which variant an Attribute holds. It mirrors the four player attribute types FlexMatch supports: string, number, string_list, and string_number_map.
type Attributes ¶
type Attributes = core.Attributes
Attributes is a player's attribute bag, keyed by attribute name. The keys must match the names declared in the rule set's playerAttributes block for the rules to find them.
type Clock ¶
Clock is the source of "now" used by the matchmaker.
It exists so that callers — and especially tests — can control what time looks like. All time-dependent behaviour in flexi (computing how long a ticket has been waiting, deciding which expansion step is active) reads the current time through this interface rather than calling time.Now directly.
Implementations must be safe for concurrent use.
type FakeClock ¶
type FakeClock struct {
// contains filtered or unexported fields
}
FakeClock is a Clock whose value is set explicitly and never advances on its own. It is intended for tests: construct one with NewFakeClock, then call FakeClock.Advance or FakeClock.Set to move time forward between matchmaker ticks.
FakeClock is safe for concurrent use.
func NewFakeClock ¶
NewFakeClock returns a FakeClock anchored at t.
Choose any wall-clock value you find convenient for diagnostics; flexi only looks at differences between Now() readings, never at absolute time.
func (*FakeClock) Advance ¶
Advance moves the FakeClock forward by d.
Use this between calls to Matchmaker.Tick in tests to simulate the passage of time and trigger expansion steps.
type Match ¶
Match is a successful pairing of tickets into a complete game.
Teams maps the team name (as it appeared in the rule set) to the players assigned to that team. When a rule set declares quantity > 1 for a team, the resulting names are suffixed with "_1", "_2", and so on. TicketIDs lists every ticket consumed to form the match, sorted lexicographically for stable test output.
type Matchmaker ¶
type Matchmaker struct {
// contains filtered or unexported fields
}
Matchmaker forms matches from in-memory tickets according to a parsed FlexMatch rule set.
Construct one with New, add tickets with Matchmaker.Enqueue, and call Matchmaker.Tick periodically to drain matches that are now satisfiable.
When the rule set sets acceptanceRequired=true, Tick produces Proposal candidates instead of final [Match]es. Callers route each proposal through Matchmaker.Accept / Matchmaker.Reject and drive Matchmaker.Tick again; once every player on every ticket of a proposal has accepted, the next Tick returns the corresponding Match.
If a proposed match fails acceptance — a player rejects, or the acceptanceTimeoutSeconds window elapses — flexi mirrors AWS FlexMatch: tickets on which every player had accepted return to the queue in StatusSearching (carrying StatusReasonAcceptanceFailed) for the next Tick to re-match, while the ticket(s) that caused the failure (a player who rejected or never responded) move to StatusCancelled.
Independently, a ticket that stays in matchmaking longer than the rule set's requestTimeoutSeconds fails with StatusTimedOut. This request-level deadline applies to queued and re-queued tickets regardless of acceptanceRequired.
Matchmaker has no internal goroutines or timers — all work happens on the goroutine that calls Tick. The queue, status map, and proposals are protected by a mutex so producers may Enqueue/Cancel/Accept concurrently with a ticking loop.
func New ¶
func New(rulesetJSON []byte, opts ...Option) (*Matchmaker, error)
New parses a FlexMatch rule set JSON document and returns a Matchmaker ready to accept tickets.
rulesetJSON must be the same JSON body accepted by GameLift's CreateMatchmakingRuleSet API (the RuleSetBody parameter). Parsing or validation failures are reported as errors that wrap ErrInvalidRuleSet.
Options may be supplied to override defaults; see WithClock.
func (*Matchmaker) Accept ¶
func (m *Matchmaker) Accept(ticketID, playerID string) error
Accept records that playerID, who must be part of the ticket ticketID, has accepted the proposed match. Once every player on every ticket in the proposal has accepted, the proposal is resolved on the next [Tick]: the tickets move to StatusPlacing and the corresponding Match is returned.
Returns ErrUnknownTicket if the ticket is not tracked, ErrUnknownProposal if the ticket is not currently in a pending proposal, or ErrUnknownPlayer if playerID is not a member of the ticket.
func (*Matchmaker) Cancel ¶
func (m *Matchmaker) Cancel(ticketID string) error
Cancel removes the ticket with the given ID from the matchmaker and marks it StatusCancelled. It returns ErrUnknownTicket if no such ticket is currently tracked. A ticket that is queued (StatusQueued) or has been re-queued after a failed acceptance (StatusSearching) is removed directly.
If the ticket is part of an active proposal, the entire proposal is torn down: every sibling ticket is also marked StatusCancelled, matching FlexMatch's behaviour when any member of a proposed match drops out. Unlike a reject or acceptance timeout, a user-initiated cancel never re-queues a sibling, even one that had already accepted.
Cancelling a ticket that has already been consumed by a match (and is now StatusPlacing or StatusCompleted) is rejected with ErrUnknownTicket.
func (*Matchmaker) Enqueue ¶
func (m *Matchmaker) Enqueue(t Ticket) error
Enqueue adds t to the matchmaking queue and records its status as StatusQueued.
The ticket's EnqueuedAt field is set from the configured Clock; any value supplied by the caller is overwritten so that wait-time calculations remain consistent. The ticket must have a non-empty ID and at least one player; otherwise an error is returned and the ticket is not enqueued.
Enqueue returns ErrDuplicateTicket (wrapped) if a ticket with the same ID is already queued, in a proposal, or in a recent terminal state.
func (*Matchmaker) MarkCompleted ¶
func (m *Matchmaker) MarkCompleted(ticketID string) error
MarkCompleted transitions ticketID from StatusPlacing to StatusCompleted. This is the equivalent of a standalone-mode caller reporting that they have finished placing the match into a game session and connection details are now available on their side.
Returns ErrUnknownTicket if the ticket is not tracked or ErrTicketNotPlacing if it is in any other status.
func (*Matchmaker) Pending ¶
func (m *Matchmaker) Pending() int
Pending returns the number of tickets currently in StatusQueued. Tickets held in a proposal (StatusRequiresAcceptance) are not counted.
func (*Matchmaker) PendingAcceptances ¶
func (m *Matchmaker) PendingAcceptances() []Proposal
PendingAcceptances returns a snapshot of every proposal currently awaiting acceptance. The slice is newly allocated and safe for the caller to mutate.
func (*Matchmaker) Reject ¶
func (m *Matchmaker) Reject(ticketID, playerID string) error
Reject records a player's rejection of a proposed match, ending the proposal. The proposal's tickets are then split following AWS FlexMatch: any ticket on which every player had already accepted is returned to the queue in StatusSearching (carrying StatusReasonAcceptanceFailed) so the next Matchmaker.Tick can re-match it, while every other ticket — including the one carrying the rejecting player and any whose players never responded — moves to StatusCancelled. An acceptance timeout (see Matchmaker.Tick) splits the proposal the same way.
Errors match Matchmaker.Accept.
func (*Matchmaker) RuleMetrics ¶ added in v0.0.2
func (m *Matchmaker) RuleMetrics(ticketID string) ([]RuleMetric, bool)
RuleMetrics returns the cumulative rule-evaluation metrics accumulated for ticketID across every Tick in which it participated in match formation, together with true. If the ticket has never been involved in an evaluation (for example it was cancelled while still only queued, or no such ticket is tracked), it returns nil and false.
The metrics support FlexMatch's MatchmakingTimedOut and MatchmakingCancelled parity: like [Status], they are retained through terminal states (TIMED_OUT, CANCELLED, PLACING, COMPLETED) and are not evicted automatically. Each entry is named after a top-level rule in the rule set; the returned slice is a copy the caller may mutate freely.
func (*Matchmaker) Status ¶
func (m *Matchmaker) Status(ticketID string) (TicketStatus, error)
Status returns the current TicketStatus for ticketID. If no ticket with that id is currently tracked by the matchmaker, ErrUnknownTicket is returned.
Note that the matchmaker retains status for tickets past queue removal only while they are in a terminal state reachable from this matchmaker (CANCELLED, TIMED_OUT, PLACING, COMPLETED). Eviction of terminal state is not automatic; call sites should not rely on long-term retention.
func (*Matchmaker) StatusReason ¶ added in v0.1.0
func (m *Matchmaker) StatusReason(ticketID string) (StatusReason, bool)
StatusReason returns the supplementary StatusReason for ticketID together with true when one applies, and "", false otherwise.
A reason is currently set only while a ticket is in StatusSearching after a proposed match it had accepted failed to gather every required acceptance; in that case the reason is StatusReasonAcceptanceFailed. This lets a caller polling Matchmaker.Status distinguish a ticket re-entering matchmaking after a failed acceptance (FlexMatch's MatchmakingSearching with a status reason) from one that was freshly enqueued. The reason clears as soon as the ticket leaves StatusSearching (for example when the next Tick re-matches it).
func (*Matchmaker) Tick ¶
func (m *Matchmaker) Tick() ([]Match, error)
Tick drives the matchmaker forward by one step.
In order, Tick:
- Expires any proposals whose acceptanceTimeoutSeconds has elapsed: their fully-accepted tickets return to the queue in StatusSearching and the rest move to StatusCancelled.
- Resolves any proposals that have been fully accepted, moving tickets to StatusPlacing and returning the corresponding Match values.
- Fails any queued or re-queued ticket that has been in matchmaking longer than requestTimeoutSeconds, moving it to StatusTimedOut.
- Applies FlexMatch expansions based on the oldest queued ticket's wait time.
- Runs the matching algorithm over the remaining queued tickets. When acceptanceRequired=false, each result becomes a Match returned in this tick; when true, each result is held as a new proposal and its tickets move to StatusRequiresAcceptance.
Tick returns nil, nil when nothing resolves in this step. A non-nil error indicates a configuration problem (for example, an expansion targeting a field that no longer exists).
type Option ¶
type Option func(*config)
Option configures a Matchmaker at construction time. Pass any number of options to New.
type Player ¶
Player is a single participant in a Ticket. ID identifies the player within a match (it is echoed back in Match.Teams). Attributes carry the values referenced by rule expressions; their keys must match names declared in the rule set's playerAttributes block. Latencies maps an AWS region name to the player's measured latency in milliseconds; it is consulted by latency rules.
Players are passed by value and may be safely re-used across tickets.
type Proposal ¶
type Proposal struct {
Teams map[string][]core.Player
TicketIDs []string
CreatedAt time.Time
// RuleEvaluationMetrics holds the per-rule pass/fail tallies accumulated
// by the search that formed this candidate match, for PotentialMatchCreated
// parity. It is nil when the rule set declares no rules.
RuleEvaluationMetrics []core.RuleMetric
}
Proposal describes a candidate match awaiting player acceptance. It is returned by Matchmaker.PendingAcceptances so callers can surface the pending decision to their players.
Teams mirrors [Match.Teams] for the candidate. TicketIDs lists every ticket participating in the proposal. CreatedAt is the clock time at which the proposal was formed; together with the rule set's acceptanceTimeoutSeconds it determines when the proposal times out.
type RuleMetric ¶ added in v0.0.2
type RuleMetric = core.RuleMetric
RuleMetric reports how many times a rule passed or failed while matchmaking processed a set of tickets. It corresponds to one entry of the ruleEvaluationMetrics array in FlexMatch's PotentialMatchCreated, MatchmakingTimedOut, and MatchmakingCancelled events. RuleName matches a rule name declared in the rule set. It is exposed on Match and Proposal for a formed/candidate match, and via Matchmaker.RuleMetrics for the cumulative per-ticket totals.
type StatusReason ¶ added in v0.1.0
type StatusReason = core.StatusReason
StatusReason gives supplementary context for a ticket's status, mirroring MatchmakingTicket.StatusReason in the GameLift API. It is reported by Matchmaker.StatusReason.
type SystemClock ¶
type SystemClock struct{}
SystemClock is the default Clock; it returns the wall-clock time from the standard library's time.Now.
SystemClock has no state and a zero value is ready to use.
type Ticket ¶
Ticket is a matchmaking request submitted to the engine. A ticket may contain a single player (a "solo" request) or several players who must be matched together as a party.
ID must be unique among queued tickets. EnqueuedAt is filled in by Matchmaker.Enqueue from the configured Clock; any value set by the caller is overwritten.
type TicketStatus ¶
type TicketStatus = core.TicketStatus
TicketStatus is the FlexMatch-compatible ticket lifecycle state.
Values mirror the MatchmakingTicket.Status values in the AWS GameLift API. Because this library runs FlexMatch in standalone mode (no game session placement), the terminal success status is StatusPlacing; callers that have attached connection information to the ticket may promote it to StatusCompleted via Matchmaker.MarkCompleted.
Transitions:
- Enqueue → StatusQueued
- Tick forms a candidate (acceptanceRequired=true) → StatusRequiresAcceptance
- All players Accept → StatusPlacing (Match returned)
- acceptanceRequired=false + Tick → StatusPlacing (Match returned)
- MarkCompleted → StatusCompleted
- Cancel on queued / searching → StatusCancelled
- Reject / acceptance timeout: rejecting or non-responding ticket → StatusCancelled; fully-accepted siblings → StatusSearching (re-queued)
- requestTimeoutSeconds elapsed → StatusTimedOut
StatusSearching is assigned to a ticket that is returned to the queue after a proposed match it accepted failed to gather every required acceptance (see Matchmaker.StatusReason); such a ticket is re-considered by the next Matchmaker.Tick. StatusTimedOut is reached when a ticket exceeds the rule set's requestTimeoutSeconds while in matchmaking — note that an acceptance failure (reject or acceptance timeout) does not produce it; those terminate as StatusCancelled, matching FlexMatch. StatusFailed is defined for parity with FlexMatch but is not produced by the current implementation.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
algorithm
Package algorithm builds matches from a queue of tickets according to a FlexMatch rule set's algorithm settings.
|
Package algorithm builds matches from a queue of tickets according to a FlexMatch rule set's algorithm settings. |
|
core
Package core holds value types shared between the public flexi package and the internal sub-packages, defined here to break an import cycle.
|
Package core holds value types shared between the public flexi package and the internal sub-packages, defined here to break an import cycle. |
|
expansion
Package expansion applies time-driven loosening of rule set values, as declared by the FlexMatch "expansions" block.
|
Package expansion applies time-driven loosening of rule set values, as declared by the FlexMatch "expansions" block. |
|
expr
Package expr implements a parser and evaluator for FlexMatch property expressions used inside rule measurements and reference values, e.g.
|
Package expr implements a parser and evaluator for FlexMatch property expressions used inside rule measurements and reference values, e.g. |
|
queue
Package queue is the in-memory ticket store used by the matchmaker.
|
Package queue is the in-memory ticket store used by the matchmaker. |
|
rule
Package rule converts parsed ruleset.Rule entries into evaluators that answer "does this candidate match satisfy me?".
|
Package rule converts parsed ruleset.Rule entries into evaluators that answer "does this candidate match satisfy me?". |
|
ruleset
Package ruleset parses and validates AWS GameLift FlexMatch rule set JSON documents (the same payload accepted by CreateMatchmakingRuleSet).
|
Package ruleset parses and validates AWS GameLift FlexMatch rule set JSON documents (the same payload accepted by CreateMatchmakingRuleSet). |