mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-10-07 19:37:51 +00:00
518 lines
17 KiB
Go
518 lines
17 KiB
Go
// Package balancer is an implementation of the controller.Balancer interface.
|
|
package balancer
|
|
|
|
import (
|
|
"log"
|
|
"math"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/dax"
|
|
"github.com/featurebasedb/featurebase/v3/dax/controller"
|
|
"github.com/featurebasedb/featurebase/v3/errors"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
)
|
|
|
|
// Ensure type implements interface.
|
|
var _ controller.Balancer = (*Balancer)(nil)
|
|
|
|
// TODO: continue figuring out which things we removed need to come back and implement them, based on what's not implemented on the balancer interface
|
|
|
|
// Balancer is an implementation of the balancer.Balancer interface which
|
|
// isolates workers and jobs by database. It helps manage the relationships
|
|
// between workers and jobs. The logic it uses to balance jobs across workers is
|
|
// very simple; it bases everything off the number of workers and number of
|
|
// jobs. It does not take anything else (such as job size, worker capabilities,
|
|
// etc) into consideration.
|
|
type Balancer struct {
|
|
store controller.Store
|
|
|
|
logger logger.Logger
|
|
}
|
|
|
|
// New returns a new instance of Balancer.
|
|
func New(store controller.Store, logger logger.Logger) *Balancer {
|
|
return &Balancer{
|
|
store: store,
|
|
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// AddWorker adds the given Node to the Balancer's available worker pool. Note
|
|
// that a node is used for ALL of the role types specified. In other words,
|
|
// specifying roleTypes = {compute, translate}, does not mean that the node can
|
|
// be used as either a compute worker or a translate worker. It means that it
|
|
// will be used as both.
|
|
func (b *Balancer) AddWorker(tx dax.Transaction, node *dax.Node) ([]dax.WorkerDiff, error) {
|
|
b.logger.Debugf("AddWorker(%s)", node.Address)
|
|
|
|
qdbidp, err := b.store.AddWorker(tx, node)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "creating node on node service: %s", node.Address)
|
|
}
|
|
|
|
diffs, err := b.balanceDatabase(tx, *qdbidp)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "balancing db")
|
|
}
|
|
return diffs.Output(), nil
|
|
}
|
|
|
|
// RemoveWorker removes a worker from the system. If the worker is currently
|
|
// assigned to a database and has jobs, it will be removed and its jobs will
|
|
// be either transferred to other workers or placed on the free job list.
|
|
func (b *Balancer) RemoveWorker(tx dax.Transaction, addr dax.Address) ([]dax.WorkerDiff, error) {
|
|
diffs := controller.NewInternalDiffs()
|
|
|
|
worker, err := b.store.WorkerForAddress(tx, addr)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting worker for address")
|
|
}
|
|
|
|
// drop from workers table. This *should* also remove all associations of jobs to this worker.
|
|
if err := b.store.RemoveWorker(tx, worker.ID); err != nil {
|
|
return nil, errors.Wrap(err, "removing worker")
|
|
}
|
|
|
|
if worker.DatabaseID != nil {
|
|
// Balance the affected database.
|
|
if diff, err := b.balanceDatabase(tx, *worker.DatabaseID); err != nil {
|
|
return nil, errors.Wrapf(err, "balancing database: %s", worker.DatabaseID)
|
|
} else {
|
|
diffs.Merge(diff)
|
|
}
|
|
}
|
|
|
|
return diffs.Output(), nil
|
|
}
|
|
|
|
func (b *Balancer) AddJobs(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID, jobs ...dax.Job) ([]dax.WorkerDiff, error) {
|
|
start := time.Now()
|
|
defer func() {
|
|
log.Printf("ELAPSED: Balancer.AddJob: %v", time.Since(start))
|
|
}()
|
|
|
|
switch len(jobs) {
|
|
case 0:
|
|
// No jobs so return early.
|
|
b.logger.Debugf("%s: AddJobs (no jobs provided)", roleType)
|
|
return []dax.WorkerDiff{}, nil
|
|
case 1:
|
|
b.logger.Debugf("%s: AddJobs (%s)", roleType, jobs[0])
|
|
default:
|
|
b.logger.Debugf("%s: AddJobs (%d)", roleType, len(jobs))
|
|
}
|
|
|
|
// TODO(tlt): we don't currently use "table" in this method; even though we
|
|
// pass a table, we're still encoding the tableKey in the job. In theory, we
|
|
// could exclude tableKey from the job coming into this method, and add it
|
|
// here.
|
|
dbid := qtid.QualifiedDatabaseID.DatabaseID
|
|
|
|
diff, err := b.addJobs(tx, roleType, dbid, jobs...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "adding job")
|
|
}
|
|
|
|
return diff.Output(), nil
|
|
}
|
|
|
|
func (b *Balancer) addJobs(tx dax.Transaction, roleType dax.RoleType, dbid dax.DatabaseID, jobs ...dax.Job) (controller.InternalDiffs, error) {
|
|
diffs := controller.NewInternalDiffs()
|
|
|
|
if len(jobs) == 0 {
|
|
return diffs, nil
|
|
}
|
|
|
|
if cnt, err := b.store.WorkerCount(tx, roleType, dbid); err != nil {
|
|
return nil, errors.Wrap(err, "getting worker count")
|
|
} else if cnt == 0 {
|
|
if err := b.store.CreateFreeJobs(tx, roleType, dbid, jobs...); err != nil {
|
|
return nil, errors.Wrap(err, "creating free jobs")
|
|
}
|
|
}
|
|
|
|
diff, err := b.addDatabaseJobs(tx, roleType, dbid, jobs...)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "adding database jobs: (%s) %s", roleType, dbid)
|
|
}
|
|
diffs.Merge(diff)
|
|
|
|
return diffs, nil
|
|
}
|
|
|
|
// addDatabaseJobs adds the job for the provided database. TODO, bad doc
|
|
func (b *Balancer) addDatabaseJobs(tx dax.Transaction, roleType dax.RoleType, dbid dax.DatabaseID, jobs ...dax.Job) (controller.InternalDiffs, error) {
|
|
workerJobs, err := b.store.WorkersJobs(tx, roleType, dbid)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "getting workers jobs: %s", roleType)
|
|
}
|
|
|
|
jset := dax.NewSet[dax.Job]()
|
|
addrToID := make(map[dax.Address]dax.WorkerID)
|
|
for _, workerInfo := range workerJobs {
|
|
addrToID[workerInfo.Address] = workerInfo.ID
|
|
jset.Merge(dax.NewSet(workerInfo.Jobs...))
|
|
}
|
|
|
|
addrs := make(dax.Addresses, 0, len(workerJobs))
|
|
jobCounts := make(map[dax.Address]int, 0)
|
|
for _, v := range workerJobs {
|
|
addrs = append(addrs, v.Address)
|
|
jobCounts[v.Address] = len(v.Jobs)
|
|
}
|
|
|
|
diffs := controller.NewInternalDiffs()
|
|
|
|
jobsToCreate := make(map[dax.Address][]dax.Job)
|
|
|
|
for _, job := range jobs {
|
|
// Skip any job that already exists.
|
|
if jset.Contains(job) {
|
|
continue
|
|
}
|
|
|
|
// Find the worker with the fewest number of jobs and assign it this job.
|
|
var lowCount int = math.MaxInt
|
|
var lowWorker dax.Address
|
|
|
|
// We loop over addrs here instead of jobCounts because jobCounts is a
|
|
// map and it can return results in an unexpected order, which is a
|
|
// problem for testing.
|
|
for _, addr := range addrs {
|
|
jobCount := jobCounts[addr]
|
|
if jobCount < lowCount {
|
|
lowCount = jobCount
|
|
lowWorker = addr
|
|
}
|
|
}
|
|
|
|
jobsToCreate[lowWorker] = append(jobsToCreate[lowWorker], job)
|
|
jobCounts[lowWorker]++
|
|
}
|
|
|
|
for addr, jobs := range jobsToCreate {
|
|
if err := b.store.AssignWorkerToJobs(tx, roleType, dbid, addrToID[addr], jobs...); err != nil {
|
|
return nil, errors.Wrap(err, "creating job")
|
|
}
|
|
for _, job := range jobs {
|
|
diffs.Added(addr, job)
|
|
}
|
|
}
|
|
|
|
return diffs, nil
|
|
}
|
|
|
|
func (b *Balancer) CurrentState(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) ([]dax.WorkerInfo, error) {
|
|
dbid := qdbid.DatabaseID
|
|
return b.store.WorkersJobs(tx, roleType, dbid)
|
|
}
|
|
|
|
func (b *Balancer) WorkerState(tx dax.Transaction, roleType dax.RoleType, addr dax.Address) (dax.WorkerInfo, error) {
|
|
return b.store.WorkerJobs(tx, roleType, addr)
|
|
}
|
|
|
|
func (b *Balancer) RemoveJobs(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID) ([]dax.WorkerDiff, error) {
|
|
diffs, err := b.store.DeleteJobsForTable(tx, roleType, qtid)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "deleting jobs from storage")
|
|
}
|
|
return diffs.Output(), nil
|
|
}
|
|
|
|
// TODO can probably simplify this with better SQL
|
|
func (b *Balancer) WorkersForJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, jobs ...dax.Job) ([]dax.WorkerInfo, error) {
|
|
out := make(map[dax.Address]dax.Set[dax.Job])
|
|
|
|
workerJobs, err := b.store.WorkersJobs(tx, roleType, qdbid.DatabaseID)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "getting worker jobs: (%s) %s", roleType, qdbid)
|
|
}
|
|
for _, workerInfo := range workerJobs {
|
|
jset := dax.NewSet(workerInfo.Jobs...)
|
|
|
|
matches := dax.NewSet[dax.Job]()
|
|
for _, job := range jobs {
|
|
if jset.Contains(job) {
|
|
matches.Add(job)
|
|
}
|
|
}
|
|
|
|
if len(matches) > 0 {
|
|
out[workerInfo.Address] = matches
|
|
}
|
|
}
|
|
|
|
workers := make([]dax.WorkerInfo, 0, len(out))
|
|
|
|
for addr, jset := range out {
|
|
workers = append(workers, dax.WorkerInfo{
|
|
Address: addr,
|
|
Jobs: jset.Sorted(),
|
|
})
|
|
}
|
|
|
|
sort.Sort(dax.WorkerInfos(workers))
|
|
|
|
return workers, nil
|
|
}
|
|
|
|
// TODO can probably simplify this with better SQL
|
|
func (b *Balancer) WorkersForTable(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID) ([]dax.WorkerInfo, error) {
|
|
out := make(map[dax.Address]dax.Set[dax.Job])
|
|
|
|
dbid := qtid.QualifiedDatabaseID.DatabaseID
|
|
|
|
prefix := string(qtid.Key())
|
|
|
|
workerJobs, err := b.store.WorkersJobs(tx, roleType, dbid)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "getting worker jobs: (%s) %s", roleType, dbid)
|
|
}
|
|
for _, workerInfo := range workerJobs {
|
|
|
|
matches := dax.NewSet[dax.Job]()
|
|
for _, job := range workerInfo.Jobs {
|
|
if strings.HasPrefix(string(job), prefix) {
|
|
matches.Add(job)
|
|
}
|
|
}
|
|
if len(matches) > 0 {
|
|
out[workerInfo.Address] = matches
|
|
}
|
|
}
|
|
|
|
workers := make([]dax.WorkerInfo, 0, len(out))
|
|
|
|
for addr, jset := range out {
|
|
workers = append(workers, dax.WorkerInfo{
|
|
Address: addr,
|
|
Jobs: jset.Sorted(),
|
|
})
|
|
}
|
|
|
|
sort.Sort(dax.WorkerInfos(workers))
|
|
|
|
return workers, nil
|
|
}
|
|
|
|
func (b *Balancer) BalanceDatabase(tx dax.Transaction, qdbid dax.QualifiedDatabaseID) ([]dax.WorkerDiff, error) {
|
|
diffs, err := b.balanceDatabase(tx, qdbid.DatabaseID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "balancing")
|
|
}
|
|
return diffs.Output(), nil
|
|
}
|
|
|
|
func (b *Balancer) balanceDatabase(tx dax.Transaction, dbid dax.DatabaseID) (controller.InternalDiffs, error) {
|
|
diffs := controller.NewInternalDiffs()
|
|
|
|
for _, role := range dax.AllRoleTypes {
|
|
diff, err := b.balanceDatabaseForRole(tx, role, dbid)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "getting worker count: (%s) %s", role, dbid)
|
|
}
|
|
diffs.Merge(diff)
|
|
}
|
|
|
|
return diffs, nil
|
|
}
|
|
|
|
func (b *Balancer) balanceDatabaseForRole(tx dax.Transaction, roleType dax.RoleType, dbid dax.DatabaseID) (controller.InternalDiffs, error) {
|
|
b.logger.Debugf("balancing database %s for role: %s\n", dbid, roleType)
|
|
diffs := controller.NewInternalDiffs()
|
|
|
|
// If there are no workers, we can't properly balance.
|
|
if cnt, err := b.store.WorkerCount(tx, roleType, dbid); err != nil {
|
|
return nil, errors.Wrapf(err, "getting worker count: (%s) %s", roleType, dbid)
|
|
} else if cnt == 0 {
|
|
return controller.InternalDiffs{}, errors.Errorf("unexpected? no workers for database %v (TODO: this used to silently return nil)", dbid)
|
|
}
|
|
|
|
// Process the freeJobs.
|
|
if diff, err := b.processFreeJobs(tx, roleType, dbid); err != nil {
|
|
return nil, errors.Wrapf(err, "processing free jobs: (%s) %s", roleType, dbid)
|
|
} else {
|
|
diffs.Merge(diff)
|
|
}
|
|
|
|
// Balance the jobs among workers.
|
|
diff, err := b.balanceDatabaseJobs(tx, roleType, dbid, diffs)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "balancing jobs")
|
|
}
|
|
|
|
return diff, nil
|
|
}
|
|
|
|
// balanceDatabaseJobs moves jobs among workers with the goal of having an equal
|
|
// number of jobs per worker. This method takes an `internalDiffs` as input for
|
|
// cases where some action has preceeded this call which also resulted in
|
|
// `internalDiffs`. Instead of having this method take a value, we could rely on
|
|
// the internalDiffs.merge() method, but we would need to modify that method to
|
|
// be smarter about the order in which it applies the add/remove operations.
|
|
// Until that's in place, we'll pass in a value here.
|
|
func (b *Balancer) balanceDatabaseJobs(tx dax.Transaction, roleType dax.RoleType, dbid dax.DatabaseID, diffs controller.InternalDiffs) (controller.InternalDiffs, error) {
|
|
workerInfos, err := b.store.WorkersJobs(tx, roleType, dbid)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "getting current state: (%s) %s", roleType, dbid)
|
|
}
|
|
|
|
numWorkers := len(workerInfos)
|
|
if numWorkers == 0 {
|
|
return nil, errors.Errorf("can't balance a database ID %v with no workers", dbid)
|
|
}
|
|
numJobs := 0
|
|
for _, info := range workerInfos {
|
|
numJobs += len(info.Jobs)
|
|
}
|
|
|
|
minJobsPerWorker := numJobs / numWorkers
|
|
numWorkersAboveMin := numJobs % numWorkers
|
|
|
|
removedJobs := make(dax.Jobs, 0)
|
|
addedJobs := make(map[dax.WorkerID]dax.Jobs)
|
|
|
|
// remove jobs loop
|
|
//
|
|
// Find all the workers that have more jobs than the target and
|
|
// add those jobs to the list of removedJobs.
|
|
for i, workerInfo := range workerInfos {
|
|
numTargetJobs := minJobsPerWorker
|
|
if i < numWorkersAboveMin {
|
|
numTargetJobs += 1
|
|
}
|
|
|
|
numCurrentJobs := len(workerInfo.Jobs)
|
|
// If we don't need to remove jobs from this worker, then just continue
|
|
// on to the next worker.
|
|
if numCurrentJobs <= numTargetJobs {
|
|
continue
|
|
}
|
|
|
|
for i := numTargetJobs; i < numCurrentJobs; i++ {
|
|
diffs.Removed(workerInfo.Address, workerInfo.Jobs[i])
|
|
}
|
|
removedJobs = append(removedJobs, workerInfo.Jobs[numTargetJobs:]...)
|
|
}
|
|
|
|
// add jobs loop
|
|
//
|
|
// Find all the workers that have fewer jobs than the target, and
|
|
// add jobs to them from the removedJobs list. This should balance
|
|
// perfectly such that removedJobs is empty at the end of this
|
|
// loop.
|
|
for i, workerInfo := range workerInfos {
|
|
numTargetJobs := minJobsPerWorker
|
|
if i < numWorkersAboveMin {
|
|
numTargetJobs += 1
|
|
}
|
|
|
|
numCurrentJobs := len(workerInfo.Jobs)
|
|
// If current jobs is already at the target, we don't need to
|
|
// add jobs to this worker, so continue.
|
|
if numCurrentJobs >= numTargetJobs {
|
|
continue
|
|
}
|
|
|
|
for i := 0; i < numTargetJobs-numCurrentJobs; i++ {
|
|
diffs.Added(workerInfo.Address, removedJobs[0])
|
|
addedJobs[workerInfo.ID] = append(addedJobs[workerInfo.ID], removedJobs[0])
|
|
removedJobs = removedJobs[1:]
|
|
}
|
|
}
|
|
if len(removedJobs) != 0 {
|
|
panic("everything is terrible")
|
|
}
|
|
|
|
for wid, jobs := range addedJobs {
|
|
err := b.store.AssignWorkerToJobs(tx, roleType, dbid, wid, jobs...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "assigning worker to jobs in DB")
|
|
}
|
|
}
|
|
|
|
return diffs, nil
|
|
}
|
|
|
|
// processFreeJobs assigns all jobs in the free list to a worker.
|
|
func (b *Balancer) processFreeJobs(tx dax.Transaction, roleType dax.RoleType, dbid dax.DatabaseID) (controller.InternalDiffs, error) {
|
|
diffs := controller.NewInternalDiffs()
|
|
jobs, err := b.store.ListFreeJobs(tx, roleType, dbid)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "listing free jobs: %s", roleType)
|
|
}
|
|
|
|
if aj, err := b.addDatabaseJobs(tx, roleType, dbid, jobs...); err != nil {
|
|
return nil, errors.Wrapf(err, "adding jobs: %s", jobs)
|
|
} else {
|
|
diffs.Merge(aj)
|
|
}
|
|
|
|
return diffs, nil
|
|
}
|
|
|
|
func (b *Balancer) ReadNode(tx dax.Transaction, addr dax.Address) (*dax.Node, error) {
|
|
return b.store.Worker(tx, addr)
|
|
}
|
|
func (b *Balancer) Nodes(tx dax.Transaction) ([]*dax.Node, error) {
|
|
return b.store.Workers(tx)
|
|
}
|
|
|
|
func (b *Balancer) CreateWorkerServiceProvider(tx dax.Transaction, sp dax.WorkerServiceProvider) error {
|
|
return b.store.CreateWorkerServiceProvider(tx, sp)
|
|
}
|
|
|
|
func (b *Balancer) CreateWorkerService(tx dax.Transaction, srv dax.WorkerService) error {
|
|
return b.store.CreateWorkerService(tx, srv)
|
|
}
|
|
|
|
func (b *Balancer) WorkerServiceProviders(tx dax.Transaction /*, future optional filters */) (dax.WorkerServiceProviders, error) {
|
|
return b.store.WorkerServiceProviders(tx)
|
|
}
|
|
|
|
func (b *Balancer) AssignFreeServiceToDatabase(tx dax.Transaction, wspID dax.WorkerServiceProviderID, qdb *dax.QualifiedDatabase) (*dax.WorkerService, error) {
|
|
return b.store.AssignFreeServiceToDatabase(tx, wspID, qdb)
|
|
}
|
|
|
|
func (b *Balancer) WorkerServices(tx dax.Transaction, wsp dax.WorkerServiceProviderID) (dax.WorkerServices, error) {
|
|
return b.store.WorkerServices(tx, wsp)
|
|
}
|
|
|
|
type WorkerJobService interface {
|
|
WorkersJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) ([]dax.WorkerInfo, error)
|
|
|
|
WorkerCount(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (int, error)
|
|
ListWorkers(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (dax.Addresses, error)
|
|
|
|
AssignWorkerToJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job ...dax.Job) error
|
|
DeleteJob(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job dax.Job) error
|
|
DeleteJobsForTable(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID) (controller.InternalDiffs, error)
|
|
JobCounts(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr ...dax.Address) (map[dax.Address]int, error)
|
|
ListJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) (dax.Jobs, error)
|
|
|
|
DatabaseForWorker(tx dax.Transaction, addr dax.Address) dax.DatabaseKey
|
|
}
|
|
|
|
type FreeJobService interface {
|
|
CreateJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, job ...dax.Job) error
|
|
DeleteJob(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, job dax.Job) error
|
|
DeleteJobsForTable(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID) error
|
|
ListJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (dax.Jobs, error)
|
|
MarkJobsAsFree(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, jobs dax.Jobs) error
|
|
}
|
|
|
|
type FreeWorkerService interface {
|
|
PopWorkers(tx dax.Transaction, roleType dax.RoleType, num int) ([]dax.Address, error)
|
|
ListWorkers(tx dax.Transaction, roleType dax.RoleType) (dax.Addresses, error)
|
|
}
|
|
|
|
type WorkerServiceProviderService interface {
|
|
CreateWorkerServiceProvider(tx dax.Transaction, sp dax.WorkerServiceProvider) error
|
|
CreateWorkerService(tx dax.Transaction, srv dax.WorkerService) error
|
|
WorkerServiceProviders(tx dax.Transaction /*, future optional filters */) (dax.WorkerServiceProviders, error)
|
|
WorkerServices(tx dax.Transaction, wspID dax.WorkerServiceProviderID) (dax.WorkerServices, error)
|
|
AssignFreeServiceToDatabase(tx dax.Transaction, wspID dax.WorkerServiceProviderID, qdb *dax.QualifiedDatabase) (*dax.WorkerService, error)
|
|
}
|