/* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef ASSET_STREAM_MANAGER_MULTI_SESSION_H_ #define ASSET_STREAM_MANAGER_MULTI_SESSION_H_ #include #include #include #include #include "absl/status/status.h" #include "absl/status/statusor.h" #include "asset_stream_manager/asset_stream_server.h" #include "asset_stream_manager/metrics_recorder.h" #include "asset_stream_manager/session_config.h" #include "common/stopwatch.h" #include "data_store/data_store_writer.h" #include "manifest/file_chunk_map.h" #include "manifest/manifest_updater.h" namespace cdc_ft { class ProcessFactory; class Session; using ManifestUpdatedCb = std::function; // Updates the manifest and runs a file watcher in a background thread. class MultiSessionRunner { public: // |src_dir| is the source directory on the workstation to stream. // |data_store| can be passed for tests to override the default store used. // |process_factory| abstracts process creation. // |enable_stats| shows whether statistics should be derived. // |wait_duration| is the waiting time for changes in the streamed directory. // |num_updater_threads| is the thread count for the manifest updater. // |manifest_updated_cb| is the callback executed when a new manifest is set. MultiSessionRunner( std::string src_dir, DataStoreWriter* data_store, ProcessFactory* process_factory, bool enable_stats, absl::Duration wait_duration, uint32_t num_updater_threads, MultiSessionMetricsRecorder const* metrics_recorder, ManifestUpdatedCb manifest_updated_cb = ManifestUpdatedCb()); ~MultiSessionRunner() = default; // Starts |server_| of |type| on |port|. absl::Status Initialize( int port, AssetStreamServerType type, ContentSentHandler content_sent = ContentSentHandler()); // Stops updating the manifest and |server_|. absl::Status Shutdown() ABSL_LOCKS_EXCLUDED(mutex_); // Waits until a manifest is ready and the gamelet |instance_id| has // acknowledged the reception of the currently set manifest id. |fuse_timeout| // is the timeout for waiting for the FUSE manifest ack. The time required to // generate the manifest is not part of this timeout as this could take a // longer time for a directory with many files. absl::Status WaitForManifestAck(const std::string& instance_id, absl::Duration fuse_timeout); absl::Status Status() ABSL_LOCKS_EXCLUDED(mutex_); // Returns the current manifest id used by |server_|. ContentIdProto ManifestId() const; private: // Updates manifest if the content of the watched directory changes and // distributes it to subscribed gamelets. void Run(); // Record MultiSessionStart event. void RecordMultiSessionStart(const ManifestUpdater& manifest_updater); // Record ManifestUpdate event. void RecordManifestUpdate(const ManifestUpdater& manifest_updater, absl::Duration duration, metrics::UpdateTrigger trigger, absl::Status status); void SetStatus(absl::Status status) ABSL_LOCKS_EXCLUDED(mutex_); // Files changed callback called from FileWatcherWin. void OnFilesChanged() ABSL_LOCKS_EXCLUDED(mutex_); // Directory recreated callback called from FileWatcherWin. void OnDirRecreated() ABSL_LOCKS_EXCLUDED(mutex_); // Called during manifest update when the intermediate manifest or the final // manifest is available. Pushes the manifest to connected FUSEs. void SetManifest(const ContentIdProto& manifest_id); const std::string src_dir_; DataStoreWriter* const data_store_; ProcessFactory* const process_factory_; FileChunkMap file_chunks_; const absl::Duration wait_duration_; const uint32_t num_updater_threads_; const ManifestUpdatedCb manifest_updated_cb_; std::unique_ptr server_; std::unique_ptr manifest_updater_; // Modifications (shutdown, file changes). absl::Mutex mutex_; bool shutdown_ ABSL_GUARDED_BY(mutex_) = false; bool files_changed_ ABSL_GUARDED_BY(mutex_) = false; bool dir_recreated_ ABSL_GUARDED_BY(mutex_) = false; bool manifest_set_ ABSL_GUARDED_BY(mutex_) = false; Stopwatch files_changed_timer_ ABSL_GUARDED_BY(mutex_); absl::Status status_ ABSL_GUARDED_BY(mutex_); // Background thread that watches files and updates the manifest. std::unique_ptr thread_; MultiSessionMetricsRecorder const* metrics_recorder_; }; // Manages an asset streaming session from a fixed directory on the workstation // to an arbitrary number of gamelets. class MultiSession { public: // Maximum length of cache path. We must be able to write content hashes into // this path: // \01234567890123456789 = 260 characters. static constexpr size_t kDefaultMaxCachePathLen = 260 - 1 - ContentId::kHashSize * 2 - 1; // Length of the hash appended to the cache directory, exposed for testing. static constexpr size_t kDirHashLen = 8; // |src_dir| is the source directory on the workstation to stream. // |cfg| contains generic configuration parameters for each session. // |process_factory| abstracts process creation. // |data_store| can be passed for tests to override the default store used. // By default, the class uses a DiskDataStore that writes to // %APPDATA%\GGP\asset_streaming| on Windows. MultiSession(std::string src_dir, SessionConfig cfg, ProcessFactory* process_factory, MultiSessionMetricsRecorder const* metrics_recorder, std::unique_ptr data_store = nullptr); ~MultiSession(); // Initializes the data store if not overridden in the constructor and starts // a background thread for updating the manifest and watching file changes. // Does not wait for the initial manifest update to finish. Use IsRunning() // to determine whether it is finished. // Not thread-safe. absl::Status Initialize(); // Stops all sessions and shuts down the server. // Not thread-safe. absl::Status Shutdown() ABSL_LOCKS_EXCLUDED(shutdownMu_); // Returns the |src_dir| streaming directory passed to the constructor. const std::string& src_dir() const { return src_dir_; } // Returns the status of the background thread. // Not thread-safe. absl::Status Status(); // Starts a new streaming session to the instance with given |instance_id| and // waits until the FUSE has received the initial manifest id. // Returns an error if a session for that instance already exists. // |instance_id| is the instance id of the target remote instance. // |project_id| is id of the project that contains the instance. // |organization_id| is id of the organization that contains the instance. // |instance_ip| is the IP address of the instance. // |instance_port| is the SSH port for connecting to the remote instance. // Thread-safe. absl::Status StartSession(const std::string& instance_id, const std::string& project_id, const std::string& organization_id, const std::string& instance_ip, uint16_t instance_port) ABSL_LOCKS_EXCLUDED(sessions_mutex_); // Starts a new streaming session to the gamelet with given |instance_id|. // Returns a NotFound error if a session for that instance does not exists. // Thread-safe. absl::Status StopSession(const std::string& instance_id) ABSL_LOCKS_EXCLUDED(sessions_mutex_); // Returns true if there is an existing session for |instance_id|. bool HasSessionForInstance(const std::string& instance_id) ABSL_LOCKS_EXCLUDED(sessions_mutex_); // Returns true if the FUSE process is up and running for an existing session // with ID |instance_id|. bool IsSessionHealthy(const std::string& instance_id) ABSL_LOCKS_EXCLUDED(sessions_mutex_); // Returns true if the MultiSession does not have any active sessions. bool Empty() ABSL_LOCKS_EXCLUDED(sessions_mutex_); // Returns the number of avtive sessions. uint32_t GetSessionCount() ABSL_LOCKS_EXCLUDED(sessions_mutex_); // For a given source directory |dir|, e.g. "C:\path\to\game", returns a // sanitized version of |dir| plus a hash of |dir|, e.g. // "c__path_to_game_abcdef01". static std::string GetCacheDir(std::string dir); // Returns the directory where manifest chunks are cached, e.g. // "%APPDATA%\GGP\asset_streaming\c__path_to_game_abcdef01" for // "C:\path\to\game". // The returned path is shortened to |max_len| by removing UTF8 code points // from the beginning of the actual cache directory (c__path...) if necessary. static absl::StatusOr GetCachePath( const std::string& src_dir, size_t max_len = kDefaultMaxCachePathLen); // Record an event associated with the multi-session. void RecordMultiSessionEvent(metrics::DeveloperLogEvent event, metrics::EventType code); // Record an event for a session associated with the |instance|. void RecordSessionEvent(metrics::DeveloperLogEvent event, metrics::EventType code, const std::string& instance_id); private: std::string src_dir_; SessionConfig cfg_; ProcessFactory* const process_factory_; std::unique_ptr data_store_; std::thread heartbeat_watcher_; absl::Mutex shutdownMu_; bool shutdown_ ABSL_GUARDED_BY(shutdownMu_) = false; // Background thread for watching file changes and updating the manifest. std::unique_ptr runner_; // Local forwarding port for the asset stream service. int local_asset_stream_port_ = 0; // Maps instance id to sessions. std::unordered_map> sessions_ ABSL_GUARDED_BY(sessions_mutex_); absl::Mutex sessions_mutex_; MultiSessionMetricsRecorder const* metrics_recorder_; Session* FindSession(const std::string& instance_id) ABSL_LOCKS_EXCLUDED(sessions_mutex_); void OnContentSent(size_t byte_count, size_t chunck_count, std::string instance_id); void StartHeartBeatCheck(); }; } // namespace cdc_ft #endif // ASSET_STREAM_MANAGER_MULTI_SESSION_H_