github.com/status-im/status-go@v1.1.0/protocol/messenger_store_node_request_manager.go (about) 1 package protocol 2 3 import ( 4 "database/sql" 5 "fmt" 6 "strings" 7 "sync" 8 "time" 9 10 "github.com/status-im/status-go/eth-node/crypto" 11 "github.com/status-im/status-go/protocol/common/shard" 12 13 "go.uber.org/zap" 14 15 "github.com/status-im/status-go/eth-node/types" 16 "github.com/status-im/status-go/protocol/communities" 17 "github.com/status-im/status-go/protocol/transport" 18 "github.com/status-im/status-go/services/mailservers" 19 ) 20 21 const ( 22 storeNodeAvailableTimeout = 30 * time.Second 23 ) 24 25 // StoreNodeRequestStats is used in tests 26 type StoreNodeRequestStats struct { 27 FetchedEnvelopesCount int 28 FetchedPagesCount int 29 } 30 31 type storeNodeRequestID struct { 32 RequestType storeNodeRequestType `json:"requestType"` 33 DataID string `json:"dataID"` 34 } 35 36 func (r *storeNodeRequestID) getCommunityID() string { 37 switch r.RequestType { 38 case storeNodeCommunityRequest: 39 return r.DataID 40 case storeNodeShardRequest: 41 return strings.TrimSuffix(r.DataID, transport.CommunityShardInfoTopicPrefix()) 42 default: 43 return "" 44 } 45 } 46 47 type StoreNodeRequestManager struct { 48 messenger *Messenger 49 logger *zap.Logger 50 51 // activeRequests contain all ongoing store node requests. 52 // Map is indexed with `DataID`. 53 // Request might be duplicated in the map in case of contentType collisions. 54 activeRequests map[storeNodeRequestID]*storeNodeRequest 55 56 // activeRequestsLock should be locked each time activeRequests is being accessed or changed. 57 activeRequestsLock sync.RWMutex 58 59 onPerformingBatch func(MailserverBatch) 60 } 61 62 func NewStoreNodeRequestManager(m *Messenger) *StoreNodeRequestManager { 63 return &StoreNodeRequestManager{ 64 messenger: m, 65 logger: m.logger.Named("StoreNodeRequestManager"), 66 activeRequests: map[storeNodeRequestID]*storeNodeRequest{}, 67 activeRequestsLock: sync.RWMutex{}, 68 onPerformingBatch: nil, 69 } 70 } 71 72 // FetchCommunity makes a single request to store node for a given community id/shard pair. 73 // When a community is successfully fetched, a `CommunityFound` event will be emitted. If `waitForResponse == true`, 74 // the function will also wait for the store node response and return the fetched community. 75 // Automatically waits for an available store node. 76 // When a `nil` community and `nil` error is returned, that means the community wasn't found at the store node. 77 func (m *StoreNodeRequestManager) FetchCommunity(community communities.CommunityShard, opts []StoreNodeRequestOption) (*communities.Community, StoreNodeRequestStats, error) { 78 cfg := buildStoreNodeRequestConfig(opts) 79 80 m.logger.Info("requesting community from store node", 81 zap.Any("community", community), 82 zap.Any("config", cfg)) 83 84 requestCommunity := func(communityID string, shard *shard.Shard) (*communities.Community, StoreNodeRequestStats, error) { 85 channel, err := m.subscribeToRequest(storeNodeCommunityRequest, communityID, shard, cfg) 86 if err != nil { 87 return nil, StoreNodeRequestStats{}, fmt.Errorf("failed to create a request for community: %w", err) 88 } 89 90 if !cfg.WaitForResponse { 91 return nil, StoreNodeRequestStats{}, nil 92 } 93 94 result := <-channel 95 return result.community, result.stats, result.err 96 } 97 98 // if shard was not passed or nil, request shard first 99 communityShard := community.Shard 100 if communityShard == nil { 101 id := transport.CommunityShardInfoTopic(community.CommunityID) 102 fetchedShard, err := m.subscribeToRequest(storeNodeShardRequest, id, shard.DefaultNonProtectedShard(), cfg) 103 if err != nil { 104 return nil, StoreNodeRequestStats{}, fmt.Errorf("failed to create a shard info request: %w", err) 105 } 106 107 if !cfg.WaitForResponse { 108 go func() { 109 shardResult := <-fetchedShard 110 communityShard = shardResult.shard 111 112 _, _, _ = requestCommunity(community.CommunityID, communityShard) 113 }() 114 return nil, StoreNodeRequestStats{}, nil 115 } 116 117 shardResult := <-fetchedShard 118 communityShard = shardResult.shard 119 } 120 121 // request community with on shard 122 return requestCommunity(community.CommunityID, communityShard) 123 } 124 125 // FetchCommunities makes a FetchCommunity for each element in given `communities` list. 126 // For each successfully fetched community, a `CommunityFound` event will be emitted. Ability to subscribe 127 // to results is not provided, because it's not needed and would complicate the code. `FetchCommunity` can 128 // be called directly if such functionality is needed. 129 // 130 // This function intentionally doesn't fetch multiple content topics in a single store node request. For now 131 // FetchCommunities is only used for regular (once in 2 minutes) fetching of curated communities. If one of 132 // those content topics is spammed with to many envelopes, then on each iteration we will have to fetch all 133 // of this spam first to get the envelopes in other content topics. To avoid this we keep independent requests 134 // for each content topic. 135 func (m *StoreNodeRequestManager) FetchCommunities(communities []communities.CommunityShard, opts []StoreNodeRequestOption) error { 136 m.logger.Info("requesting communities from store node", zap.Any("communities", communities)) 137 138 // when fetching multiple communities we don't wait for the response 139 opts = append(opts, WithWaitForResponseOption(false)) 140 141 var outErr error 142 143 for _, community := range communities { 144 _, _, err := m.FetchCommunity(community, opts) 145 if err != nil { 146 outErr = fmt.Errorf("%sfailed to create a request for community %s: %w", outErr, community.CommunityID, err) 147 } 148 } 149 150 return outErr 151 } 152 153 // FetchContact - similar to FetchCommunity 154 // If a `nil` contact and a `nil` error are returned, it means that the contact wasn't found at the store node. 155 func (m *StoreNodeRequestManager) FetchContact(contactID string, opts []StoreNodeRequestOption) (*Contact, StoreNodeRequestStats, error) { 156 157 cfg := buildStoreNodeRequestConfig(opts) 158 159 m.logger.Info("requesting contact from store node", 160 zap.Any("contactID", contactID), 161 zap.Any("config", cfg)) 162 163 channel, err := m.subscribeToRequest(storeNodeContactRequest, contactID, nil, cfg) 164 if err != nil { 165 return nil, StoreNodeRequestStats{}, fmt.Errorf("failed to create a request for community: %w", err) 166 } 167 168 if !cfg.WaitForResponse { 169 return nil, StoreNodeRequestStats{}, nil 170 } 171 172 result := <-channel 173 return result.contact, result.stats, result.err 174 } 175 176 // subscribeToRequest checks if a request for given community/contact is already in progress, creates and installs 177 // a new one if not found, and returns a subscription to the result of the found/started request. 178 // The subscription can then be used to get the result of the request, this could be either a community/contact or an error. 179 func (m *StoreNodeRequestManager) subscribeToRequest(requestType storeNodeRequestType, dataID string, shard *shard.Shard, cfg StoreNodeRequestConfig) (storeNodeResponseSubscription, error) { 180 // It's important to unlock only after getting the subscription channel. 181 // We also lock `activeRequestsLock` during finalizing the requests. This ensures that the subscription 182 // created in this function will get the result even if the requests proceeds faster than this function ends. 183 m.activeRequestsLock.Lock() 184 defer m.activeRequestsLock.Unlock() 185 186 requestID := storeNodeRequestID{ 187 RequestType: requestType, 188 DataID: dataID, 189 } 190 191 request, requestFound := m.activeRequests[requestID] 192 193 if !requestFound { 194 // Create corresponding filter 195 var err error 196 var filter *transport.Filter 197 filterCreated := false 198 199 filter, filterCreated, err = m.getFilter(requestType, dataID, shard) 200 if err != nil { 201 if filterCreated { 202 m.forgetFilter(filter) 203 } 204 return nil, fmt.Errorf("failed to create community filter: %w", err) 205 } 206 207 request = m.newStoreNodeRequest() 208 request.config = cfg 209 request.pubsubTopic = filter.PubsubTopic 210 request.requestID = requestID 211 request.contentTopic = filter.ContentTopic 212 if filterCreated { 213 request.filterToForget = filter 214 } 215 216 m.activeRequests[requestID] = request 217 request.start() 218 } 219 220 return request.subscribe(), nil 221 } 222 223 // newStoreNodeRequest creates a new storeNodeRequest struct 224 func (m *StoreNodeRequestManager) newStoreNodeRequest() *storeNodeRequest { 225 return &storeNodeRequest{ 226 manager: m, 227 subscriptions: make([]storeNodeResponseSubscription, 0), 228 } 229 } 230 231 // getFilter checks if a filter for a given community is already created and creates one of not found. 232 // Returns the found/created filter, a flag if the filter was created by the function and an error. 233 func (m *StoreNodeRequestManager) getFilter(requestType storeNodeRequestType, dataID string, shard *shard.Shard) (*transport.Filter, bool, error) { 234 // First check if such filter already exists. 235 filter := m.messenger.transport.FilterByChatID(dataID) 236 if filter != nil { 237 //we don't remember filter id associated with community because it was already installed 238 return filter, false, nil 239 } 240 241 switch requestType { 242 case storeNodeShardRequest, storeNodeCommunityRequest: 243 // If filter wasn't installed we create it and 244 // remember for uninstalling after response is received 245 filters, err := m.messenger.transport.InitPublicFilters([]transport.FiltersToInitialize{{ 246 ChatID: dataID, 247 PubsubTopic: shard.PubsubTopic(), 248 }}) 249 250 if err != nil { 251 m.logger.Error("failed to install filter for community", zap.Error(err)) 252 return nil, false, err 253 } 254 255 if len(filters) != 1 { 256 m.logger.Error("Unexpected number of filters created") 257 return nil, true, fmt.Errorf("unexepcted number of filters created") 258 } 259 260 filter = filters[0] 261 case storeNodeContactRequest: 262 publicKeyBytes, err := types.DecodeHex(dataID) 263 if err != nil { 264 return nil, false, fmt.Errorf("failed to decode contact id: %w", err) 265 } 266 267 publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes) 268 if err != nil { 269 return nil, false, fmt.Errorf("failed to unmarshal public key: %w", err) 270 } 271 272 filter, err = m.messenger.transport.JoinPrivate(publicKey) 273 274 if err != nil { 275 return nil, false, fmt.Errorf("failed to install filter for contact: %w", err) 276 } 277 278 default: 279 return nil, false, fmt.Errorf("invalid store node request type: %d", requestType) 280 } 281 282 filter.Ephemeral = true 283 284 return filter, true, nil 285 } 286 287 // forgetFilter uninstalls the given filter 288 func (m *StoreNodeRequestManager) forgetFilter(filter *transport.Filter) { 289 err := m.messenger.transport.RemoveFilters([]*transport.Filter{filter}) 290 if err != nil { 291 m.logger.Warn("failed to remove filter", zap.Error(err)) 292 } 293 } 294 295 type storeNodeRequestType int 296 297 const ( 298 storeNodeCommunityRequest storeNodeRequestType = iota 299 storeNodeContactRequest 300 storeNodeShardRequest 301 ) 302 303 // storeNodeRequest represents a single store node batch request. 304 // For a valid storeNodeRequest to be performed, the user must set all the struct fields and call start method. 305 type storeNodeRequest struct { 306 requestID storeNodeRequestID 307 308 // request parameters 309 pubsubTopic string 310 contentTopic types.TopicType 311 minimumDataClock uint64 312 config StoreNodeRequestConfig 313 314 // request corresponding metadata to be used in finalize 315 filterToForget *transport.Filter 316 317 // internal fields 318 manager *StoreNodeRequestManager 319 subscriptions []storeNodeResponseSubscription 320 result storeNodeRequestResult 321 } 322 323 // storeNodeRequestResult contains result of a single storeNodeRequest 324 // Further by using `data` we mean community/contact, depending on request type. 325 // If any error occurs during the request, err field will be set. 326 // If data was successfully fetched, data field will contain the fetched information. 327 // If data wasn't found in store node, then a data will be set to `nil`. 328 // stats will contain information about the performed request that might be useful for testing. 329 type storeNodeRequestResult struct { 330 err error 331 stats StoreNodeRequestStats 332 // One of data fields (community or contact) will be present depending on request type 333 community *communities.Community 334 contact *Contact 335 shard *shard.Shard 336 } 337 338 type storeNodeResponseSubscription = chan storeNodeRequestResult 339 340 func (r *storeNodeRequest) subscribe() storeNodeResponseSubscription { 341 channel := make(storeNodeResponseSubscription, 100) 342 r.subscriptions = append(r.subscriptions, channel) 343 return channel 344 } 345 346 func (r *storeNodeRequest) finalize() { 347 r.manager.activeRequestsLock.Lock() 348 defer r.manager.activeRequestsLock.Unlock() 349 350 r.manager.logger.Info("request finished", 351 zap.Any("requestID", r.requestID), 352 zap.Bool("communityFound", r.result.community != nil), 353 zap.Bool("contactFound", r.result.contact != nil), 354 zap.Bool("shardFound", r.result.shard != nil), 355 zap.Error(r.result.err)) 356 357 // Send the result to subscribers 358 // It's important that this is done with `activeRequestsLock` locked. 359 for _, s := range r.subscriptions { 360 s <- r.result 361 close(s) 362 } 363 364 if r.result.community != nil { 365 r.manager.messenger.passStoredCommunityInfoToSignalHandler(r.result.community) 366 } 367 368 delete(r.manager.activeRequests, r.requestID) 369 370 if r.filterToForget != nil { 371 r.manager.forgetFilter(r.filterToForget) 372 } 373 } 374 375 func (r *storeNodeRequest) shouldFetchNextPage(envelopesCount int) (bool, uint32) { 376 logger := r.manager.logger.With( 377 zap.Any("requestID", r.requestID), 378 zap.Int("envelopesCount", envelopesCount)) 379 380 r.result.stats.FetchedEnvelopesCount += envelopesCount 381 r.result.stats.FetchedPagesCount++ 382 383 // Force all received envelopes to be processed 384 r.manager.messenger.ProcessAllMessages() 385 386 // Try to get community from database 387 switch r.requestID.RequestType { 388 case storeNodeCommunityRequest: 389 communityID, err := types.DecodeHex(r.requestID.DataID) 390 if err != nil { 391 logger.Error("failed to decode community ID", 392 zap.String("communityID", r.requestID.DataID), 393 zap.Error(err)) 394 r.result = storeNodeRequestResult{ 395 community: nil, 396 err: fmt.Errorf("failed to decode community ID: %w", err), 397 } 398 return false, 0 // failed to decode community ID, no sense to continue the procedure 399 } 400 401 // check if community is waiting for a verification and do a verification manually 402 _, err = r.manager.messenger.communitiesManager.ValidateCommunityByID(communityID) 403 if err != nil { 404 logger.Error("failed to validate community by ID", 405 zap.String("communityID", r.requestID.DataID), 406 zap.Error(err)) 407 r.result = storeNodeRequestResult{ 408 community: nil, 409 err: fmt.Errorf("failed to validate community by ID: %w", err), 410 } 411 return false, 0 // failed to validate community, no sense to continue the procedure 412 } 413 414 community, err := r.manager.messenger.communitiesManager.GetByID(communityID) 415 416 if err != nil && err != communities.ErrOrgNotFound { 417 logger.Error("failed to read community from database", 418 zap.String("communityID", r.requestID.DataID), 419 zap.Error(err)) 420 r.result = storeNodeRequestResult{ 421 community: nil, 422 err: fmt.Errorf("failed to read community from database: %w", err), 423 } 424 return false, 0 // failed to read from database, no sense to continue the procedure 425 } 426 427 if community == nil { 428 // community not found in the database, request next page 429 logger.Debug("community still not fetched") 430 return true, r.config.FurtherPageSize 431 } 432 433 // We check here if the community was fetched actually fetched and updated, because it 434 // could be that the community was already in the database when we started the fetching. 435 // 436 // Would be perfect if we could track that the community was in these particular envelopes, 437 // but I don't think that's possible right now. We check if clock was updated instead. 438 439 if community.Clock() <= r.minimumDataClock { 440 logger.Debug("local community description is not newer than existing", 441 zap.Any("existingClock", community.Clock()), 442 zap.Any("minimumDataClock", r.minimumDataClock), 443 ) 444 return true, r.config.FurtherPageSize 445 } 446 447 logger.Debug("community found", 448 zap.String("displayName", community.Name())) 449 450 r.result.community = community 451 452 case storeNodeShardRequest: 453 communityIDStr := strings.TrimSuffix(r.requestID.DataID, transport.CommunityShardInfoTopicPrefix()) 454 communityID, err := types.DecodeHex(communityIDStr) 455 if err != nil { 456 logger.Error("decode community ID failed", 457 zap.String("communityID", communityIDStr), 458 zap.Error(err)) 459 return false, 0 460 } 461 shardResult, err := r.manager.messenger.communitiesManager.GetCommunityShard(communityID) 462 if err != nil { 463 if err != sql.ErrNoRows { 464 logger.Error("failed to read from database", 465 zap.String("communityID", communityIDStr), 466 zap.Error(err)) 467 r.result = storeNodeRequestResult{ 468 shard: nil, 469 err: fmt.Errorf("failed to read from database: %w", err), 470 } 471 return false, 0 // failed to read from database, no sense to continue the procedure 472 } 473 } 474 475 logger.Debug("shard found", 476 zap.String("community", communityIDStr), 477 zap.Any("shard", shardResult), 478 ) 479 480 r.result.shard = shardResult 481 482 case storeNodeContactRequest: 483 contact := r.manager.messenger.GetContactByID(r.requestID.DataID) 484 485 if contact == nil { 486 // contact not found in the database, request next page 487 logger.Debug("contact still not fetched") 488 return true, r.config.FurtherPageSize 489 } 490 491 logger.Debug("contact found", 492 zap.String("displayName", contact.DisplayName)) 493 494 r.result.contact = contact 495 } 496 497 return !r.config.StopWhenDataFound, r.config.FurtherPageSize 498 } 499 500 func (r *storeNodeRequest) routine() { 501 r.manager.logger.Info("starting store node request", 502 zap.Any("requestID", r.requestID), 503 zap.String("pubsubTopic", r.pubsubTopic), 504 zap.Any("contentTopic", r.contentTopic), 505 ) 506 507 // Return a nil community and no error when request was 508 // performed successfully, but no community/contact found. 509 r.result = storeNodeRequestResult{ 510 err: nil, 511 community: nil, 512 contact: nil, 513 shard: nil, 514 } 515 516 defer func() { 517 r.finalize() 518 }() 519 520 communityID := r.requestID.getCommunityID() 521 522 if r.requestID.RequestType != storeNodeCommunityRequest || !r.manager.messenger.communityStorenodes.HasStorenodeSetup(communityID) { 523 if !r.manager.messenger.waitForAvailableStoreNode(storeNodeAvailableTimeout) { 524 r.result.err = fmt.Errorf("store node is not available") 525 return 526 } 527 } 528 529 storeNode := r.manager.messenger.getActiveMailserver(communityID) 530 531 // Check if community already exists locally and get Clock. 532 if r.requestID.RequestType == storeNodeCommunityRequest { 533 localCommunity, _ := r.manager.messenger.communitiesManager.GetByIDString(communityID) 534 if localCommunity != nil { 535 r.minimumDataClock = localCommunity.Clock() 536 } 537 } 538 539 // Start store node request 540 from, to := r.manager.messenger.calculateMailserverTimeBounds(oneMonthDuration) 541 542 _, err := r.manager.messenger.performMailserverRequest(storeNode, func(ms mailservers.Mailserver) (*MessengerResponse, error) { 543 batch := MailserverBatch{ 544 From: from, 545 To: to, 546 PubsubTopic: r.pubsubTopic, 547 Topics: []types.TopicType{r.contentTopic}, 548 } 549 r.manager.logger.Info("perform store node request", zap.Any("batch", batch)) 550 if r.manager.onPerformingBatch != nil { 551 r.manager.onPerformingBatch(batch) 552 } 553 554 return nil, r.manager.messenger.processMailserverBatchWithOptions(ms, batch, r.config.InitialPageSize, r.shouldFetchNextPage, true) 555 }) 556 557 r.result.err = err 558 } 559 560 func (r *storeNodeRequest) start() { 561 go r.routine() 562 }