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 }