Track active projects in metrics

An active project is defined as a project where there has been at
least a buffer edit, a join request/response, or a follow update
in the last minute.
This commit is contained in:
Antonio Scandurra 2022-06-15 10:33:08 +02:00
parent 6d93a41f40
commit 3a1d0dd692
5 changed files with 97 additions and 22 deletions

View file

@ -61,8 +61,10 @@ pub use store::{Store, Worktree};
lazy_static! { lazy_static! {
static ref METRIC_CONNECTIONS: IntGauge = static ref METRIC_CONNECTIONS: IntGauge =
register_int_gauge!("connections", "number of connections").unwrap(); register_int_gauge!("connections", "number of connections").unwrap();
static ref METRIC_PROJECTS: IntGauge = static ref METRIC_REGISTERED_PROJECTS: IntGauge =
register_int_gauge!("projects", "number of open projects").unwrap(); register_int_gauge!("registered_projects", "number of registered projects").unwrap();
static ref METRIC_ACTIVE_PROJECTS: IntGauge =
register_int_gauge!("active_projects", "number of active projects").unwrap();
static ref METRIC_SHARED_PROJECTS: IntGauge = register_int_gauge!( static ref METRIC_SHARED_PROJECTS: IntGauge = register_int_gauge!(
"shared_projects", "shared_projects",
"number of open projects with one or more guests" "number of open projects with one or more guests"
@ -159,6 +161,7 @@ impl Server {
.add_message_handler(Server::leave_project) .add_message_handler(Server::leave_project)
.add_message_handler(Server::respond_to_join_project_request) .add_message_handler(Server::respond_to_join_project_request)
.add_message_handler(Server::update_project) .add_message_handler(Server::update_project)
.add_message_handler(Server::register_project_activity)
.add_request_handler(Server::update_worktree) .add_request_handler(Server::update_worktree)
.add_message_handler(Server::start_language_server) .add_message_handler(Server::start_language_server)
.add_message_handler(Server::update_language_server) .add_message_handler(Server::update_language_server)
@ -844,6 +847,16 @@ impl Server {
Ok(()) Ok(())
} }
async fn register_project_activity(
self: Arc<Server>,
request: TypedEnvelope<proto::RegisterProjectActivity>,
) -> Result<()> {
self.store_mut()
.await
.register_project_activity(request.payload.project_id, request.sender_id)?;
Ok(())
}
async fn update_worktree( async fn update_worktree(
self: Arc<Server>, self: Arc<Server>,
request: TypedEnvelope<proto::UpdateWorktree>, request: TypedEnvelope<proto::UpdateWorktree>,
@ -1001,10 +1014,12 @@ impl Server {
request: TypedEnvelope<proto::UpdateBuffer>, request: TypedEnvelope<proto::UpdateBuffer>,
response: Response<proto::UpdateBuffer>, response: Response<proto::UpdateBuffer>,
) -> Result<()> { ) -> Result<()> {
let receiver_ids = self let receiver_ids = {
.store() let mut store = self.store_mut().await;
.await store.register_project_activity(request.payload.project_id, request.sender_id)?;
.project_connection_ids(request.payload.project_id, request.sender_id)?; store.project_connection_ids(request.payload.project_id, request.sender_id)?
};
broadcast(request.sender_id, receiver_ids, |connection_id| { broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone()) .forward_send(request.sender_id, connection_id, request.payload.clone())
@ -1065,14 +1080,18 @@ impl Server {
) -> Result<()> { ) -> Result<()> {
let leader_id = ConnectionId(request.payload.leader_id); let leader_id = ConnectionId(request.payload.leader_id);
let follower_id = request.sender_id; let follower_id = request.sender_id;
if !self
.store()
.await
.project_connection_ids(request.payload.project_id, follower_id)?
.contains(&leader_id)
{ {
Err(anyhow!("no such peer"))?; let mut store = self.store_mut().await;
if store
.project_connection_ids(request.payload.project_id, follower_id)?
.contains(&leader_id)
{
Err(anyhow!("no such peer"))?;
}
store.register_project_activity(request.payload.project_id, follower_id)?;
} }
let mut response_payload = self let mut response_payload = self
.peer .peer
.forward_request(request.sender_id, leader_id, request.payload) .forward_request(request.sender_id, leader_id, request.payload)
@ -1086,14 +1105,14 @@ impl Server {
async fn unfollow(self: Arc<Self>, request: TypedEnvelope<proto::Unfollow>) -> Result<()> { async fn unfollow(self: Arc<Self>, request: TypedEnvelope<proto::Unfollow>) -> Result<()> {
let leader_id = ConnectionId(request.payload.leader_id); let leader_id = ConnectionId(request.payload.leader_id);
if !self let mut store = self.store_mut().await;
.store() if !store
.await
.project_connection_ids(request.payload.project_id, request.sender_id)? .project_connection_ids(request.payload.project_id, request.sender_id)?
.contains(&leader_id) .contains(&leader_id)
{ {
Err(anyhow!("no such peer"))?; Err(anyhow!("no such peer"))?;
} }
store.register_project_activity(request.payload.project_id, request.sender_id)?;
self.peer self.peer
.forward_send(request.sender_id, leader_id, request.payload)?; .forward_send(request.sender_id, leader_id, request.payload)?;
Ok(()) Ok(())
@ -1103,10 +1122,10 @@ impl Server {
self: Arc<Self>, self: Arc<Self>,
request: TypedEnvelope<proto::UpdateFollowers>, request: TypedEnvelope<proto::UpdateFollowers>,
) -> Result<()> { ) -> Result<()> {
let connection_ids = self let mut store = self.store_mut().await;
.store() store.register_project_activity(request.payload.project_id, request.sender_id)?;
.await let connection_ids =
.project_connection_ids(request.payload.project_id, request.sender_id)?; store.project_connection_ids(request.payload.project_id, request.sender_id)?;
let leader_id = request let leader_id = request
.payload .payload
.variant .variant
@ -1574,12 +1593,14 @@ impl<'a> Drop for StoreWriteGuard<'a> {
let metrics = self.metrics(); let metrics = self.metrics();
METRIC_CONNECTIONS.set(metrics.connections as _); METRIC_CONNECTIONS.set(metrics.connections as _);
METRIC_PROJECTS.set(metrics.registered_projects as _); METRIC_REGISTERED_PROJECTS.set(metrics.registered_projects as _);
METRIC_ACTIVE_PROJECTS.set(metrics.active_projects as _);
METRIC_SHARED_PROJECTS.set(metrics.shared_projects as _); METRIC_SHARED_PROJECTS.set(metrics.shared_projects as _);
tracing::info!( tracing::info!(
connections = metrics.connections, connections = metrics.connections,
registered_projects = metrics.registered_projects, registered_projects = metrics.registered_projects,
active_projects = metrics.active_projects,
shared_projects = metrics.shared_projects, shared_projects = metrics.shared_projects,
"metrics" "metrics"
); );

View file

@ -9,6 +9,7 @@ use std::{
mem, mem,
path::{Path, PathBuf}, path::{Path, PathBuf},
str, str,
time::{Duration, Instant},
}; };
use tracing::instrument; use tracing::instrument;
@ -41,6 +42,8 @@ pub struct Project {
pub active_replica_ids: HashSet<ReplicaId>, pub active_replica_ids: HashSet<ReplicaId>,
pub worktrees: BTreeMap<u64, Worktree>, pub worktrees: BTreeMap<u64, Worktree>,
pub language_servers: Vec<proto::LanguageServer>, pub language_servers: Vec<proto::LanguageServer>,
#[serde(skip)]
last_activity: Option<Instant>,
} }
#[derive(Default, Serialize)] #[derive(Default, Serialize)]
@ -84,6 +87,7 @@ pub struct LeftProject {
pub struct Metrics { pub struct Metrics {
pub connections: usize, pub connections: usize,
pub registered_projects: usize, pub registered_projects: usize,
pub active_projects: usize,
pub shared_projects: usize, pub shared_projects: usize,
} }
@ -91,13 +95,17 @@ impl Store {
pub fn metrics(&self) -> Metrics { pub fn metrics(&self) -> Metrics {
let connections = self.connections.values().filter(|c| !c.admin).count(); let connections = self.connections.values().filter(|c| !c.admin).count();
let mut registered_projects = 0; let mut registered_projects = 0;
let mut active_projects = 0;
let mut shared_projects = 0; let mut shared_projects = 0;
for project in self.projects.values() { for project in self.projects.values() {
if let Some(connection) = self.connections.get(&project.host_connection_id) { if let Some(connection) = self.connections.get(&project.host_connection_id) {
if !connection.admin { if !connection.admin {
registered_projects += 1; registered_projects += 1;
if !project.guests.is_empty() { if project.is_active() {
shared_projects += 1; active_projects += 1;
if !project.guests.is_empty() {
shared_projects += 1;
}
} }
} }
} }
@ -106,6 +114,7 @@ impl Store {
Metrics { Metrics {
connections, connections,
registered_projects, registered_projects,
active_projects,
shared_projects, shared_projects,
} }
} }
@ -318,6 +327,7 @@ impl Store {
active_replica_ids: Default::default(), active_replica_ids: Default::default(),
worktrees: Default::default(), worktrees: Default::default(),
language_servers: Default::default(), language_servers: Default::default(),
last_activity: None,
}, },
); );
if let Some(connection) = self.connections.get_mut(&host_connection_id) { if let Some(connection) = self.connections.get_mut(&host_connection_id) {
@ -338,6 +348,7 @@ impl Store {
.get_mut(&project_id) .get_mut(&project_id)
.ok_or_else(|| anyhow!("no such project"))?; .ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection_id == connection_id { if project.host_connection_id == connection_id {
project.last_activity = Some(Instant::now());
let mut old_worktrees = mem::take(&mut project.worktrees); let mut old_worktrees = mem::take(&mut project.worktrees);
for worktree in worktrees { for worktree in worktrees {
if let Some(old_worktree) = old_worktrees.remove(&worktree.id) { if let Some(old_worktree) = old_worktrees.remove(&worktree.id) {
@ -460,6 +471,7 @@ impl Store {
.get_mut(&project_id) .get_mut(&project_id)
.ok_or_else(|| anyhow!("no such project"))?; .ok_or_else(|| anyhow!("no such project"))?;
connection.requested_projects.insert(project_id); connection.requested_projects.insert(project_id);
project.last_activity = Some(Instant::now());
project project
.join_requests .join_requests
.entry(requester_id) .entry(requester_id)
@ -484,6 +496,8 @@ impl Store {
let requester_connection = self.connections.get_mut(&receipt.sender_id)?; let requester_connection = self.connections.get_mut(&receipt.sender_id)?;
requester_connection.requested_projects.remove(&project_id); requester_connection.requested_projects.remove(&project_id);
} }
project.last_activity = Some(Instant::now());
Some(receipts) Some(receipts)
} }
@ -515,6 +529,7 @@ impl Store {
receipts_with_replica_ids.push((receipt, replica_id)); receipts_with_replica_ids.push((receipt, replica_id));
} }
project.last_activity = Some(Instant::now());
Some((receipts_with_replica_ids, project)) Some((receipts_with_replica_ids, project))
} }
@ -565,6 +580,8 @@ impl Store {
} }
} }
project.last_activity = Some(Instant::now());
Ok(LeftProject { Ok(LeftProject {
host_connection_id: project.host_connection_id, host_connection_id: project.host_connection_id,
host_user_id: project.host_user_id, host_user_id: project.host_user_id,
@ -653,6 +670,25 @@ impl Store {
.ok_or_else(|| anyhow!("no such project")) .ok_or_else(|| anyhow!("no such project"))
} }
pub fn register_project_activity(
&mut self,
project_id: u64,
connection_id: ConnectionId,
) -> Result<()> {
let project = self
.projects
.get_mut(&project_id)
.ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection_id == connection_id
|| project.guests.contains_key(&connection_id)
{
project.last_activity = Some(Instant::now());
Ok(())
} else {
Err(anyhow!("no such project"))?
}
}
pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Result<&Project> { pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Result<&Project> {
let project = self let project = self
.projects .projects
@ -758,6 +794,13 @@ impl Store {
} }
impl Project { impl Project {
fn is_active(&self) -> bool {
const ACTIVE_PROJECT_TIMEOUT: Duration = Duration::from_secs(60);
self.last_activity.map_or(false, |last_activity| {
last_activity.elapsed() < ACTIVE_PROJECT_TIMEOUT
})
}
pub fn guest_connection_ids(&self) -> Vec<ConnectionId> { pub fn guest_connection_ids(&self) -> Vec<ConnectionId> {
self.guests.keys().copied().collect() self.guests.keys().copied().collect()
} }

View file

@ -1780,6 +1780,10 @@ impl Project {
operations: vec![language::proto::serialize_operation(&operation)], operations: vec![language::proto::serialize_operation(&operation)],
}); });
cx.background().spawn(request).detach_and_log_err(cx); cx.background().spawn(request).detach_and_log_err(cx);
} else if let Some(project_id) = self.remote_id() {
let _ = self
.client
.send(proto::RegisterProjectActivity { project_id });
} }
} }
BufferEvent::Edited { .. } => { BufferEvent::Edited { .. } => {

View file

@ -36,6 +36,7 @@ message Envelope {
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 29; OpenBufferForSymbolResponse open_buffer_for_symbol_response = 29;
UpdateProject update_project = 30; UpdateProject update_project = 30;
RegisterProjectActivity register_project_activity = 31;
UpdateWorktree update_worktree = 32; UpdateWorktree update_worktree = 32;
CreateProjectEntry create_project_entry = 33; CreateProjectEntry create_project_entry = 33;
@ -135,6 +136,10 @@ message UpdateProject {
repeated WorktreeMetadata worktrees = 2; repeated WorktreeMetadata worktrees = 2;
} }
message RegisterProjectActivity {
uint64 project_id = 1;
}
message RequestJoinProject { message RequestJoinProject {
uint64 requester_id = 1; uint64 requester_id = 1;
uint64 project_id = 2; uint64 project_id = 2;

View file

@ -134,6 +134,7 @@ messages!(
(Ping, Foreground), (Ping, Foreground),
(ProjectUnshared, Foreground), (ProjectUnshared, Foreground),
(RegisterProject, Foreground), (RegisterProject, Foreground),
(RegisterProjectActivity, Foreground),
(ReloadBuffers, Foreground), (ReloadBuffers, Foreground),
(ReloadBuffersResponse, Foreground), (ReloadBuffersResponse, Foreground),
(RemoveProjectCollaborator, Foreground), (RemoveProjectCollaborator, Foreground),
@ -236,6 +237,7 @@ entity_messages!(
PerformRename, PerformRename,
PrepareRename, PrepareRename,
ProjectUnshared, ProjectUnshared,
RegisterProjectActivity,
ReloadBuffers, ReloadBuffers,
RemoveProjectCollaborator, RemoveProjectCollaborator,
RenameProjectEntry, RenameProjectEntry,