/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/// The driver for the clients engine. Internal; split out from the `Engine` /// struct to make testing easier. struct Driver<'a> {
command_processor: &'a dyn CommandProcessor,
interruptee: &'a dyn Interruptee,
config: &'a InfoConfiguration,
recent_clients: HashMap<String, RemoteClient>,
}
for bso in inbound { self.interruptee.err_if_interrupted()?;
let content = bso.into_content();
let client: ClientRecord = match content.kind {
IncomingKind::Malformed => {
log::debug!("Error unpacking record"); continue;
}
IncomingKind::Tombstone => {
log::debug!("Record has been deleted; skipping..."); continue;
}
IncomingKind::Content(client) => client,
};
if client.id == self.command_processor.settings().fxa_device_id {
log::debug!("Found my record on the server"); // If we see our own client record, apply any incoming commands, // remove them from the list, and reupload the record. Any // commands that we don't understand also go back in the list. // https://github.com/mozilla/application-services/issues/1800 // tracks if that's the right thing to do.
has_own_client_record = true; letmut current_client_record = self.current_client_record(); for c in &client.commands { let status = match c.as_command() {
Some(command) => self.command_processor.apply_incoming_command(command)?,
None => CommandStatus::Unsupported,
}; match status {
CommandStatus::Applied => {}
CommandStatus::Ignored => {
log::debug!("Ignored command {:?}", c);
}
CommandStatus::Unsupported => {
log::warn!("Don't know how to apply command {:?}", c);
current_client_record.commands.push(c.clone());
}
}
}
// The clients collection has a hard limit on the payload size, // after which the server starts rejecting our records. Large // command lists can cause us to exceed this, so we truncate // the list.
shrink_to_fit(
&mut current_client_record.commands, self.memcache_max_record_payload_size(),
)?;
// Add the new client record to our map of recently synced // clients, so that downstream consumers like synced tabs can // access them. self.note_recent_client(¤t_client_record);
// We periodically upload our own client record, even if it // doesn't change, to keep it fresh. if should_refresh_client || client != current_client_record {
log::debug!("Will update our client record on the server"); let envelope = OutgoingEnvelope {
id: content.envelope.id,
ttl: Some(CLIENTS_TTL),
..Default::default()
};
changes.push(OutgoingBso::from_content(envelope, current_client_record)?);
}
} else { // Add the other client to our map of recently synced clients. self.note_recent_client(&client);
// Bail if we don't have any outgoing commands to write into // the other client's record. if outgoing_commands.is_empty() { continue;
}
// Determine if we have new commands, that aren't already in the // client's command list. let current_commands: HashSet<Command> = client
.commands
.iter()
.filter_map(|c| c.as_command())
.collect(); letmut new_outgoing_commands = outgoing_commands
.difference(¤t_commands)
.cloned()
.collect::<Vec<_>>(); // Sort, to ensure deterministic ordering for tests.
new_outgoing_commands.sort(); letmut new_client = client.clone();
new_client
.commands
.extend(new_outgoing_commands.into_iter().map(CommandRecord::from)); if new_client.commands.len() == client.commands.len() { continue;
}
// Hooray, we added new commands! Make sure the record still // fits in the maximum record size, or the server will reject // our upload.
shrink_to_fit(
&mut new_client.commands, self.memcache_max_record_payload_size(),
)?;
// Upload a record for our own client, if we didn't replace it already. if !has_own_client_record { let current_client_record = self.current_client_record(); self.note_recent_client(¤t_client_record); let envelope = OutgoingEnvelope {
id: Guid::new(¤t_client_record.id),
ttl: Some(CLIENTS_TTL),
..Default::default()
};
changes.push(OutgoingBso::from_content(envelope, current_client_record)?);
}
Ok(changes)
}
/// Builds a fresh client record for this device. fn current_client_record(&self) -> ClientRecord { let settings = self.command_processor.settings();
ClientRecord {
id: settings.fxa_device_id.clone(),
name: settings.device_name.clone(),
typ: settings.device_type,
commands: Vec::new(),
fxa_device_id: Some(settings.fxa_device_id.clone()),
version: None,
protocols: vec!["1.5".into()],
form_factor: None,
os: None,
app_package: None,
application: None,
device: None,
}
}
/// Collections stored in memcached ("tabs", "clients" or "meta") have a /// different max size than ones stored in the normal storage server db. /// In practice, the real limit here is 1M (bug 1300451 comment 40), but /// there's overhead involved that is hard to calculate on the client, so we /// use 512k to be safe (at the recommendation of the server team). Note /// that if the server reports a lower limit (via info/configuration), we /// respect that limit instead. See also bug 1403052. /// XXX - the above comment is stale and refers to the world before the /// move to spanner and the rust sync server. fn memcache_max_record_payload_size(&self) -> usize { self.max_record_payload_size().min(512 * 1024)
}
}
impl Engine<'_> { /// Creates a new clients engine that delegates to the given command /// processor to apply incoming commands. pubfn new<'b>(
command_processor: &'b dyn CommandProcessor,
interruptee: &'b dyn Interruptee,
) -> Engine<'b> {
Engine {
command_processor,
interruptee,
recent_clients: HashMap::new(),
}
}
/// Syncs the clients collection. This works a little differently than /// other collections: /// /// 1. It can't be disabled or declined. /// 2. The sync ID and last sync time aren't meaningful, since we always /// fetch all client records on every sync. As such, the /// `LocalCollStateMachine` that we use for other engines doesn't /// apply to it. /// 3. It doesn't persist state directly, but relies on the sync manager /// to persist device settings, and process commands. /// 4. Failing to sync the clients collection is fatal, and aborts the /// sync. /// /// For these reasons, we implement this engine directly in the `sync15` /// crate, and provide a specialized `sync` method instead of implementing /// `sync15::Store`. pubfn sync(
&mutself,
storage_client: &Sync15StorageClient,
global_state: &GlobalState,
root_sync_key: &KeyBundle,
should_refresh_client: bool,
) -> Result<()> {
log::info!("Syncing collection clients");
log::info!( "Upload success ({} records success, {} records failed)",
upload_info.successful_ids.len(),
upload_info.failed_ids.len()
);
log::info!("Finished syncing clients");
Ok(())
}
fn fetch_incoming(
&self,
storage_client: &Sync15StorageClient,
coll_state: &CollState,
) -> Result<Vec<IncomingBso>> { // Note that, unlike other stores, we always fetch the full collection // on every sync, so `inbound` will return all clients, not just the // ones that changed since the last sync. let coll_request = CollectionRequest::new(COLLECTION_NAME.into()).full();
self.interruptee.err_if_interrupted()?; let inbound = crate::client::fetch_incoming(storage_client, coll_state, coll_request)?;
Ok(inbound)
}
pubfn local_client_id(&self) -> String { // Bit dirty but it's the easiest way to reach to our own // device ID without refactoring the whole sync manager crate. self.command_processor.settings().fxa_device_id.clone()
}
#[cfg(test)] mod tests { usesuper::super::{CommandStatus, DeviceType, Settings}; usesuper::*; usecrate::bso::IncomingBso; use anyhow::Result; use interrupt_support::NeverInterrupts; use serde_json::{json, Value}; use std::iter::zip;
// Passing false for `should_refresh_client` - it should be ignored // because we've changed the commands. letmut outgoing = driver.sync(inbound, false).expect("Should sync clients");
outgoing.sort_by(|a, b| a.envelope.id.cmp(&b.envelope.id));
// Make sure the list of recently synced remote clients is correct. let expected_ids = &["deviceAAAAAA", "deviceBBBBBB", "deviceCCCCCC"]; letmut actual_ids = driver.recent_clients.keys().collect::<Vec<&String>>();
actual_ids.sort();
assert_eq!(actual_ids, expected_ids);
// Make sure the list of recently synced remote clients is correct. let expected_ids = &["deviceAAAAAA", "deviceBBBBBB"]; letmut actual_ids = driver.recent_clients.keys().collect::<Vec<&String>>();
actual_ids.sort();
assert_eq!(actual_ids, expected_ids);
let outgoing = driver
.sync(inbound_from_clients(test_clients.clone()), false)
.expect("Should sync clients"); // should be no outgoing changes.
assert_eq!(outgoing.len(), 0);
// Make sure the list of recently synced remote clients is correct and // still includes our record we didn't update. let expected_ids = &["deviceAAAAAA", "deviceBBBBBB"]; letmut actual_ids = driver.recent_clients.keys().collect::<Vec<&String>>();
actual_ids.sort();
assert_eq!(actual_ids, expected_ids);
// Do it again - still no changes, but force a refresh. let outgoing = driver
.sync(inbound_from_clients(test_clients), true)
.expect("Should sync clients");
assert_eq!(outgoing.len(), 1);
// Do it again - but this time with our own client record needing // some change. let inbound = inbound_from_clients(json!([{ "id": "deviceAAAAAA", "name": "Laptop with New Name", "type": "desktop", "commands": [], "fxaDeviceId": "deviceAAAAAA", "protocols": ["1.5"],
}])); let outgoing = driver.sync(inbound, false).expect("Should sync clients"); // should still be outgoing because the name changed.
assert_eq!(outgoing.len(), 1);
}
let inbound = iflet Value::Array(clients) = clients {
clients
.into_iter()
.map(IncomingBso::from_test_content)
.collect()
} else {
unreachable!("`clients` must be an array of client records")
};
// Passing false here for should_refresh_client, but it should be // ignored as we don't have an existing record yet. letmut outgoing = driver.sync(inbound, false).expect("Should sync clients");
outgoing.sort_by(|a, b| a.envelope.id.cmp(&b.envelope.id));
// Make sure the list of recently synced remote clients is correct. let expected_ids = &["deviceAAAAAA", "deviceBBBBBB"]; letmut actual_ids = driver.recent_clients.keys().collect::<Vec<&String>>();
actual_ids.sort();
assert_eq!(actual_ids, expected_ids);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.