github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/dashboard/api/pkg/model/api.go (about)

     1  package model
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/filecoin-project/bacalhau/dashboard/api/pkg/store"
    10  	"github.com/filecoin-project/bacalhau/dashboard/api/pkg/types"
    11  	"github.com/filecoin-project/bacalhau/pkg/localdb"
    12  	"github.com/filecoin-project/bacalhau/pkg/localdb/postgres"
    13  	bacalhau_model "github.com/filecoin-project/bacalhau/pkg/model"
    14  	bacalhau_model_beta "github.com/filecoin-project/bacalhau/pkg/model/v1beta1"
    15  
    16  	"github.com/filecoin-project/bacalhau/pkg/node"
    17  	"github.com/filecoin-project/bacalhau/pkg/pubsub"
    18  	"github.com/filecoin-project/bacalhau/pkg/pubsub/libp2p"
    19  	"github.com/filecoin-project/bacalhau/pkg/routing"
    20  	"github.com/filecoin-project/bacalhau/pkg/routing/inmemory"
    21  	libp2p_pubsub "github.com/libp2p/go-libp2p-pubsub"
    22  	"github.com/libp2p/go-libp2p/core/host"
    23  	"github.com/rs/zerolog/log"
    24  	"golang.org/x/crypto/bcrypt"
    25  )
    26  
    27  type ModelOptions struct {
    28  	Host             host.Host
    29  	PostgresHost     string
    30  	PostgresPort     int
    31  	PostgresDatabase string
    32  	PostgresUser     string
    33  	PostgresPassword string
    34  }
    35  
    36  type ModelAPI struct {
    37  	options         ModelOptions
    38  	localDB         localdb.LocalDB
    39  	nodeDB          routing.NodeInfoStore
    40  	store           *store.PostgresStore
    41  	stateResolver   *localdb.StateResolver
    42  	jobEventHandler *jobEventHandler
    43  	cleanupFunc     func(context.Context)
    44  }
    45  
    46  func NewModelAPI(options ModelOptions) (*ModelAPI, error) {
    47  	if options.PostgresHost == "" {
    48  		return nil, fmt.Errorf("postgres host is required")
    49  	}
    50  	if options.PostgresPort == 0 {
    51  		return nil, fmt.Errorf("postgres port is required")
    52  	}
    53  	if options.PostgresDatabase == "" {
    54  		return nil, fmt.Errorf("postgres database is required")
    55  	}
    56  	if options.PostgresUser == "" {
    57  		return nil, fmt.Errorf("postgres user is required")
    58  	}
    59  	if options.PostgresPassword == "" {
    60  		return nil, fmt.Errorf("postgres password is required")
    61  	}
    62  	postgresDB, err := postgres.NewPostgresDatastore(
    63  		options.PostgresHost,
    64  		options.PostgresPort,
    65  		options.PostgresDatabase,
    66  		options.PostgresUser,
    67  		options.PostgresPassword,
    68  		true,
    69  	)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	dashboardstore, err := store.NewPostgresStore(
    74  		options.PostgresHost,
    75  		options.PostgresPort,
    76  		options.PostgresDatabase,
    77  		options.PostgresUser,
    78  		options.PostgresPassword,
    79  		true,
    80  	)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	nodeDB := inmemory.NewNodeInfoStore(inmemory.NodeInfoStoreParams{
    86  		// compute nodes publish every 30 seconds. We give a graceful period of 2 minutes for them to be considered offline
    87  		TTL: 2 * time.Minute,
    88  	})
    89  
    90  	stateResolver := localdb.GetStateResolver(postgresDB)
    91  
    92  	api := &ModelAPI{
    93  		options:         options,
    94  		localDB:         postgresDB,
    95  		nodeDB:          nodeDB,
    96  		store:           dashboardstore,
    97  		stateResolver:   stateResolver,
    98  		jobEventHandler: newJobEventHandler(postgresDB),
    99  	}
   100  	return api, nil
   101  }
   102  
   103  func (api *ModelAPI) Start(ctx context.Context) error {
   104  	if api.options.Host == nil {
   105  		return fmt.Errorf("libp2p host is required")
   106  	}
   107  	var err error
   108  	ctx, cancel := context.WithCancel(ctx)
   109  	defer func() {
   110  		if err != nil {
   111  			cancel()
   112  		}
   113  	}()
   114  
   115  	gossipSub, err := libp2p_pubsub.NewGossipSub(ctx, api.options.Host)
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	// PubSub to read node info from the network
   121  	nodeInfoPubSub, err := libp2p.NewPubSub[bacalhau_model.NodeInfo](libp2p.PubSubParams{
   122  		Host:      api.options.Host,
   123  		TopicName: node.NodeInfoTopic,
   124  		PubSub:    gossipSub,
   125  	})
   126  	if err != nil {
   127  		return err
   128  	}
   129  	err = nodeInfoPubSub.Subscribe(ctx, pubsub.SubscriberFunc[bacalhau_model.NodeInfo](api.nodeDB.Add))
   130  	if err != nil {
   131  		return err
   132  	}
   133  
   134  	// PubSub to read job events from the network
   135  	libp2p2JobEventPubSub, err := libp2p.NewPubSub[pubsub.BufferingEnvelope](libp2p.PubSubParams{
   136  		Host:      api.options.Host,
   137  		TopicName: node.JobEventsTopic,
   138  		PubSub:    gossipSub,
   139  	})
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	bufferedJobEventPubSub := pubsub.NewBufferingPubSub[bacalhau_model_beta.JobEvent](pubsub.BufferingPubSubParams{
   145  		DelegatePubSub: libp2p2JobEventPubSub,
   146  		MaxBufferAge:   5 * time.Minute, //nolint:gomnd // required, but we don't publish events in the dashboard
   147  	})
   148  	err = bufferedJobEventPubSub.Subscribe(ctx, pubsub.SubscriberFunc[bacalhau_model_beta.JobEvent](api.jobEventHandler.readEvent))
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	api.jobEventHandler.startBufferGC(ctx)
   154  	api.cleanupFunc = func(ctx context.Context) {
   155  		cleanupErr := bufferedJobEventPubSub.Close(ctx)
   156  		if cleanupErr != nil {
   157  			log.Ctx(ctx).Error().Err(cleanupErr).Msg("failed to close job event pubsub")
   158  		}
   159  		cleanupErr = libp2p2JobEventPubSub.Close(ctx)
   160  		if cleanupErr != nil {
   161  			log.Ctx(ctx).Error().Err(cleanupErr).Msg("failed to close libp2p job event pubsub")
   162  		}
   163  		cleanupErr = nodeInfoPubSub.Close(ctx)
   164  		if cleanupErr != nil {
   165  			log.Ctx(ctx).Error().Err(cleanupErr).Msg("failed to close libp2p node info pubsub")
   166  		}
   167  		cancel()
   168  	}
   169  	return nil
   170  }
   171  
   172  func (api *ModelAPI) Stop(ctx context.Context) error {
   173  	if api.cleanupFunc != nil {
   174  		api.cleanupFunc(ctx)
   175  	}
   176  	return nil
   177  }
   178  
   179  func (api *ModelAPI) GetNodes(ctx context.Context) (map[string]bacalhau_model.NodeInfo, error) {
   180  	nodesList, err := api.nodeDB.List(ctx)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	nodesMap := make(map[string]bacalhau_model.NodeInfo, len(nodesList))
   185  	for _, node := range nodesList {
   186  		if node.NodeType == bacalhau_model.NodeTypeCompute {
   187  			nodesMap[node.PeerInfo.ID.String()] = node
   188  		}
   189  	}
   190  	return nodesMap, nil
   191  }
   192  
   193  func (api *ModelAPI) GetJobs(ctx context.Context, query localdb.JobQuery) ([]*bacalhau_model_beta.Job, error) {
   194  	return api.localDB.GetJobs(ctx, query)
   195  }
   196  
   197  func (api *ModelAPI) GetJobsCount(ctx context.Context, query localdb.JobQuery) (int, error) {
   198  	return api.localDB.GetJobsCount(ctx, query)
   199  }
   200  
   201  func (api *ModelAPI) GetJob(ctx context.Context, id string) (*bacalhau_model_beta.Job, error) {
   202  	return api.localDB.GetJob(ctx, id)
   203  }
   204  
   205  func (api *ModelAPI) GetJobInfo(ctx context.Context, id string) (*types.JobInfo, error) {
   206  	info := &types.JobInfo{}
   207  
   208  	job, err := api.localDB.GetJob(ctx, id)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	info.Job = *job
   213  
   214  	// they might have asked for a short job ID so if we found a job
   215  	// let's use that for subsequent queries
   216  	loadedID := job.Metadata.ID
   217  
   218  	errorChan := make(chan error, 1)
   219  	doneChan := make(chan bool, 1)
   220  	var wg sync.WaitGroup
   221  	//nolint:gomnd
   222  	wg.Add(4)
   223  	go func() {
   224  		events, err := api.localDB.GetJobEvents(ctx, loadedID)
   225  		if err != nil {
   226  			errorChan <- err
   227  		}
   228  		info.Events = events
   229  		wg.Done()
   230  	}()
   231  	go func() {
   232  		state, err := api.stateResolver.GetJobState(ctx, loadedID)
   233  		if err != nil {
   234  			errorChan <- err
   235  		}
   236  		info.State = state
   237  		wg.Done()
   238  	}()
   239  	go func() {
   240  		results, err := api.stateResolver.GetResults(ctx, loadedID)
   241  		if err != nil {
   242  			errorChan <- err
   243  		}
   244  		info.Results = results
   245  		wg.Done()
   246  	}()
   247  	go func() {
   248  		results, err := api.GetModerationSummary(ctx, loadedID)
   249  		if err != nil {
   250  			errorChan <- err
   251  		}
   252  		info.Moderation = *results
   253  		wg.Done()
   254  	}()
   255  	go func() {
   256  		wg.Wait()
   257  		doneChan <- true
   258  	}()
   259  	select {
   260  	case <-doneChan:
   261  		return info, nil
   262  	case err := <-errorChan:
   263  		return nil, err
   264  	}
   265  }
   266  
   267  func (api *ModelAPI) GetAnnotationSummary(
   268  	ctx context.Context,
   269  ) ([]*types.AnnotationSummary, error) {
   270  	return api.store.GetAnnotationSummary(ctx)
   271  }
   272  
   273  func (api *ModelAPI) GetJobMonthSummary(
   274  	ctx context.Context,
   275  ) ([]*types.JobMonthSummary, error) {
   276  	return api.store.GetJobMonthSummary(ctx)
   277  }
   278  
   279  func (api *ModelAPI) GetJobExecutorSummary(
   280  	ctx context.Context,
   281  ) ([]*types.JobExecutorSummary, error) {
   282  	return api.store.GetJobExecutorSummary(ctx)
   283  }
   284  
   285  func (api *ModelAPI) GetTotalJobsCount(
   286  	ctx context.Context,
   287  ) (*types.Counter, error) {
   288  	return api.store.GetTotalJobsCount(ctx)
   289  }
   290  
   291  func (api *ModelAPI) GetTotalEventCount(
   292  	ctx context.Context,
   293  ) (*types.Counter, error) {
   294  	return api.store.GetTotalEventCount(ctx)
   295  }
   296  
   297  func (api *ModelAPI) GetTotalUserCount(
   298  	ctx context.Context,
   299  ) (*types.Counter, error) {
   300  	return api.store.GetTotalUserCount(ctx)
   301  }
   302  
   303  func (api *ModelAPI) GetTotalExecutorCount(
   304  	ctx context.Context,
   305  ) (*types.Counter, error) {
   306  	return api.store.GetTotalExecutorCount(ctx)
   307  }
   308  
   309  func (api *ModelAPI) AddEvent(event bacalhau_model_beta.JobEvent) error {
   310  	return api.jobEventHandler.readEvent(context.Background(), event)
   311  }
   312  
   313  func (api *ModelAPI) AddUser(
   314  	ctx context.Context,
   315  	username string,
   316  	password string,
   317  ) (*types.User, error) {
   318  	hashedPassword, err := hashPassword(password)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	err = api.store.AddUser(ctx, username, hashedPassword)
   323  	if err != nil {
   324  		return nil, err
   325  	}
   326  	return api.store.LoadUser(ctx, username)
   327  }
   328  
   329  func (api *ModelAPI) GetUser(
   330  	ctx context.Context,
   331  	username string,
   332  ) (*types.User, error) {
   333  	return api.store.LoadUser(ctx, username)
   334  }
   335  
   336  func (api *ModelAPI) UpdateUserPassword(
   337  	ctx context.Context,
   338  	username string,
   339  	password string,
   340  ) (*types.User, error) {
   341  	user, err := api.store.LoadUser(ctx, username)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  	if user == nil {
   346  		return nil, fmt.Errorf("user not found")
   347  	}
   348  	hashedPassword, err := hashPassword(password)
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  	err = api.store.UpdateUserPassword(ctx, username, hashedPassword)
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  	return api.store.LoadUser(ctx, username)
   357  }
   358  
   359  func (api *ModelAPI) Login(
   360  	ctx context.Context,
   361  	req types.LoginRequest,
   362  ) (*types.User, error) {
   363  	user, err := api.store.LoadUser(ctx, req.Username)
   364  	if err != nil || user == nil {
   365  		return nil, fmt.Errorf("incorrect details")
   366  	}
   367  	err = bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(req.Password))
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  	return user, nil
   372  }
   373  
   374  func (api *ModelAPI) GetModerationSummary(
   375  	ctx context.Context,
   376  	jobID string,
   377  ) (*types.JobModerationSummary, error) {
   378  	moderation, err := api.store.GetJobModeration(ctx, jobID)
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  	if moderation == nil {
   383  		return &types.JobModerationSummary{}, nil
   384  	}
   385  	user, err := api.store.LoadUserByID(ctx, moderation.UserAccountID)
   386  	if err != nil {
   387  		return nil, err
   388  	}
   389  	user.HashedPassword = ""
   390  	return &types.JobModerationSummary{
   391  		Moderation: moderation,
   392  		User:       user,
   393  	}, nil
   394  }
   395  
   396  func (api *ModelAPI) CreateJobModeration(
   397  	ctx context.Context,
   398  	moderation types.JobModeration,
   399  ) error {
   400  	return api.store.CreateJobModeration(ctx, moderation)
   401  }