/* 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/. */
usesuper::storage_client::Sync15ClientResponse; usecrate::bso::OutgoingEncryptedBso; usecrate::error::{self, Error as ErrorKind, Result}; usecrate::ServerTimestamp; use serde_derive::*; use std::collections::HashMap; use std::default::Default; use std::ops::Deref; use sync_guid::Guid; use viaduct::status_codes;
/// Manages a pair of (byte, count) limits for a PostQueue, such as /// (max_post_bytes, max_post_records) or (max_total_bytes, max_total_records). #[derive(Debug, Clone)] struct LimitTracker {
max_bytes: usize,
max_records: usize,
cur_bytes: usize,
cur_records: usize,
}
pubfn can_add_record(&self, payload_size: usize) -> bool { // Desktop does the cur_bytes check as exclusive, but we shouldn't see any servers that // don't have https://github.com/mozilla-services/server-syncstorage/issues/73 self.cur_records < self.max_records && self.cur_bytes + payload_size <= self.max_bytes
}
#[derive(Serialize, Deserialize, Debug, Clone)] pubstruct InfoConfiguration { /// The maximum size in bytes of the overall HTTP request body that will be accepted by the /// server. #[serde(default = "default_max_request_bytes")] pub max_request_bytes: usize,
/// The maximum number of records that can be uploaded to a collection in a single POST request. #[serde(default = "usize::max_value")] pub max_post_records: usize,
/// The maximum combined size in bytes of the record payloads that can be uploaded to a /// collection in a single POST request. #[serde(default = "usize::max_value")] pub max_post_bytes: usize,
/// The maximum total number of records that can be uploaded to a collection as part of a /// batched upload. #[serde(default = "usize::max_value")] pub max_total_records: usize,
/// The maximum total combined size in bytes of the record payloads that can be uploaded to a /// collection as part of a batched upload. #[serde(default = "usize::max_value")] pub max_total_bytes: usize,
/// The maximum size of an individual BSO payload, in bytes. #[serde(default = "default_max_record_payload_bytes")] pub max_record_payload_bytes: usize,
}
// This is annoying but seems to be the only way to do it... fn default_max_request_bytes() -> usize { 260 * 1024
} fn default_max_record_payload_bytes() -> usize { 256 * 1024
}
pubtrait BatchPoster { /// Note: Last argument (reference to the batch poster) is provided for the purposes of testing /// Important: Poster should not report non-success HTTP statuses as errors!! fn post<P, O>(
&self,
body: Vec<u8>,
xius: ServerTimestamp,
batch: Option<String>,
commit: bool,
queue: &PostQueue<P, O>,
) -> Result<PostResponse>;
}
// We don't just use a FnMut here since we want to override it in mocking for RefCell<TestType>, // which we can't do for FnMut since neither FnMut nor RefCell are defined here. Also, this // is somewhat better for documentation. pubtrait PostResponseHandler { fn handle_response(&mutself, r: PostResponse, mid_batch: bool) -> Result<()>;
}
ifself.post_limits.can_never_add(payload_length)
|| self.batch_limits.can_never_add(payload_length)
|| payload_length >= self.max_payload_bytes
{
log::warn!( "Single record too large to submit to server ({} b)",
payload_length
); return Ok(false);
}
// Write directly into `queued` but undo if necessary (the vast majority of the time // it won't be necessary). If we hit a problem we need to undo that, but the only error // case we have to worry about right now is in flush() let item_start = self.queued.len();
// This is conservative but can't hurt. self.queued.reserve(payload_length + 2);
// Either the first character in an array, or a comma separating // it from the previous item. let c = ifself.queued.is_empty() { b'[' } else { b',' }; self.queued.push(c);
// This unwrap is fine, since serde_json's failure case is HashMaps that have non-object // keys, which is impossible. If you decide to change this part, you *need* to call // `self.queued.truncate(item_start)` here in the failure case!
serde_json::to_writer(&mutself.queued, &record).unwrap();
let item_end = self.queued.len();
debug_assert!(
item_end >= payload_length, "EncryptedPayload::serialized_len is bugged"
);
// The + 1 is only relevant for the final record, which will have a trailing ']'. let item_len = item_end - item_start + 1;
if item_len >= self.max_request_bytes { self.queued.truncate(item_start);
log::warn!( "Single record too large to submit to server ({} b)",
item_len
); return Ok(false);
}
let can_post_record = self.post_limits.can_add_record(payload_length); let can_batch_record = self.batch_limits.can_add_record(payload_length); let can_send_record = self.queued.len() < self.max_request_bytes;
if !can_post_record || !can_send_record || !can_batch_record {
log::debug!( "PostQueue flushing! (can_post = {}, can_send = {}, can_batch = {})",
can_post_record,
can_send_record,
can_batch_record
); // "unwrite" the record. self.queued.truncate(item_start); // Flush whatever we have queued. self.flush(!can_batch_record)?; // And write it again. let c = ifself.queued.is_empty() { b'[' } else { b',' }; self.queued.push(c);
serde_json::to_writer(&mutself.queued, &record).unwrap();
}
pubfn flush(&mutself, want_commit: bool) -> Result<()> { ifself.queued.is_empty() {
assert!(
!self.in_batch(), "Bug: Somehow we're in a batch but have no queued records"
); // Nothing to do! return Ok(());
}
self.queued.push(b']'); let batch_id = match &self.batch { // Not the first post and we know we have no batch semantics.
BatchState::Unsupported => None, // First commit in possible batch
BatchState::NoBatch => Some("true".into()), // In a batch and we have a batch id.
BatchState::InBatch(ref s) => Some(s.clone()),
};
log::info!( "Posting {} records of {} bytes", self.post_limits.cur_records, self.queued.len()
);
let is_commit = want_commit && batch_id.is_some(); // Weird syntax for calling a function object that is a property. let resp_or_error = self.poster.post( self.queued.clone(), self.last_modified,
batch_id,
is_commit, self,
);
self.queued.truncate(0);
if want_commit || self.batch == BatchState::Unsupported { self.batch_limits.clear();
} self.post_limits.clear();
impl<Poster> PostQueue<Poster, NormalResponseHandler> { // TODO: should take by move pubfn completed_upload_info(&mutself) -> UploadInfo { letmut result = UploadInfo {
successful_ids: Vec::with_capacity(self.on_response.successful_ids.len()),
failed_ids: Vec::with_capacity( self.on_response.failed_ids.len()
+ self.on_response.pending_failed.len()
+ self.on_response.pending_success.len(),
),
modified_timestamp: self.last_modified,
};
result
.successful_ids
.append(&mutself.on_response.successful_ids);
result.failed_ids.append(&mutself.on_response.failed_ids);
result
.failed_ids
.append(&mutself.on_response.pending_failed);
result
.failed_ids
.append(&mutself.on_response.pending_success);
result
}
}
#[cfg(test)] mod test { usesuper::*; usecrate::bso::{IncomingEncryptedBso, OutgoingEncryptedBso, OutgoingEnvelope}; usecrate::EncryptedPayload; use lazy_static::lazy_static; use std::cell::RefCell; use std::collections::VecDeque; use std::rc::Rc;
impl PostedData { fn records_as_json(&self) -> Vec<serde_json::Value> { let values =
serde_json::from_str::<serde_json::Value>(&self.body).expect("Posted invalid json"); // Check that they actually deserialize as what we want let records_or_err =
serde_json::from_value::<Vec<IncomingEncryptedBso>>(values.clone());
records_or_err.expect("Failed to deserialize data");
serde_json::from_value(values).unwrap()
}
}
self.all_posts.push(post.clone()); let response = self.responses.pop_front().unwrap();
let record = match response {
Sync15ClientResponse::Success { ref record, .. } => record,
_ => {
panic!("only success codes are used in this test");
}
};
ifself.cur_batch.is_none() {
assert!(
batch.is_none() || batch == Some("true".into()), "We shouldn't be in a batch now"
); self.cur_batch = Some(BatchInfo {
id: record.batch.clone(),
posts: vec![],
records: 0,
bytes: 0,
});
} else {
assert_eq!(
batch, self.cur_batch.as_ref().unwrap().id, "We're in a batch but got the wrong batch id"
);
}
lazy_static! { // ~40b staticref PAYLOAD_OVERHEAD: usize = { let payload = EncryptedPayload {
iv: "".into(),
hmac: "".into(),
ciphertext: "".into()
};
serde_json::to_string(&payload).unwrap().len()
}; // ~80b staticref TOTAL_RECORD_OVERHEAD: usize = { let val = serde_json::to_value(OutgoingEncryptedBso::new(OutgoingEnvelope {
id: "".into(),
sortindex: None,
ttl: None,
},
EncryptedPayload {
iv: "".into(),
hmac: "".into(),
ciphertext: "".into()
},
)).unwrap();
serde_json::to_string(&val).unwrap().len()
}; // There's some subtlety in how we calculate this having to do with the fact that // the quotes in the payload are escaped but the escape chars count to the request len // and *not* to the payload len (the payload len check happens after json parsing the // top level object). staticref NON_PAYLOAD_OVERHEAD: usize = {
*TOTAL_RECORD_OVERHEAD - *PAYLOAD_OVERHEAD
};
}
// Actual record size (for max_request_len) will be larger by some amount fn make_record(payload_size: usize) -> OutgoingEncryptedBso {
assert!(payload_size > *PAYLOAD_OVERHEAD); let ciphertext_len = payload_size - *PAYLOAD_OVERHEAD;
OutgoingEncryptedBso::new(
OutgoingEnvelope {
id: "".into(),
sortindex: None,
ttl: None,
},
EncryptedPayload {
iv: "".into(),
hmac: "".into(),
ciphertext: "x".repeat(ciphertext_len),
},
)
}
let t = tester.borrow();
assert!(t.cur_batch.is_none());
assert_eq!(t.all_posts.len(), 1);
assert_eq!(t.batches.len(), 1);
assert_eq!(t.batches[0].posts.len(), 1);
assert_eq!(t.batches[0].records, 1);
assert_eq!(t.batches[0].bytes, 100);
assert_eq!(
t.batches[0].posts[0].body.len(),
request_bytes_for_payloads(&[100])
);
}
#[test] fn test_pq_max_request_bytes_no_batch() { let cfg = InfoConfiguration {
max_request_bytes: 250,
..InfoConfiguration::default()
}; let time = 11_111_111_000; let (mut pq, tester) = pq_test_setup(
cfg,
time,
vec![
fake_response(status_codes::OK, time + 100_000, None),
fake_response(status_codes::OK, time + 200_000, None),
],
);
// Note that the total record overhead is around 85 bytes let payload_size = 100 - *NON_PAYLOAD_OVERHEAD;
pq.enqueue(&make_record(payload_size)).unwrap(); // total size == 102; [r]
pq.enqueue(&make_record(payload_size)).unwrap(); // total size == 203; [r,r]
pq.enqueue(&make_record(payload_size)).unwrap(); // too big, 2nd post.
pq.flush(true).unwrap();
assert_eq!(t.batches[1].posts.len(), 1);
assert_eq!(t.batches[1].records, 1);
assert_eq!(t.batches[1].bytes, payload_size); // We know at this point that the server does not support batching.
assert_eq!(t.batches[1].posts[0].batch, None);
assert!(!t.batches[1].posts[0].commit);
assert_eq!(
t.batches[1].posts[0].body.len(),
request_bytes_for_payloads(&[payload_size])
);
}
#[test] fn test_pq_max_record_payload_bytes_no_batch() { let cfg = InfoConfiguration {
max_record_payload_bytes: 150,
max_request_bytes: 350,
..InfoConfiguration::default()
}; let time = 11_111_111_000; let (mut pq, tester) = pq_test_setup(
cfg,
time,
vec![
fake_response(status_codes::OK, time + 100_000, None),
fake_response(status_codes::OK, time + 200_000, None),
],
);
// Note that the total record overhead is around 85 bytes let payload_size = 100 - *NON_PAYLOAD_OVERHEAD;
pq.enqueue(&make_record(payload_size)).unwrap(); // total size == 102; [r] let enqueued = pq.enqueue(&make_record(151)).unwrap(); // still 102
assert!(!enqueued, "Should not have fit");
pq.enqueue(&make_record(payload_size)).unwrap();
pq.flush(true).unwrap();
#[test] fn test_pq_single_batch() { let cfg = InfoConfiguration::default(); let time = 11_111_111_000; let (mut pq, tester) = pq_test_setup(
cfg,
time,
vec![fake_response(
status_codes::ACCEPTED,
time + 100_000,
Some("1234"),
)],
);
let payload_size = 100 - *NON_PAYLOAD_OVERHEAD;
pq.enqueue(&make_record(payload_size)).unwrap();
pq.enqueue(&make_record(payload_size)).unwrap();
pq.enqueue(&make_record(payload_size)).unwrap();
pq.flush(true).unwrap();
#[test] fn test_pq_multi_post_batch_records() { let cfg = InfoConfiguration {
max_post_records: 3,
..InfoConfiguration::default()
}; let time = 11_111_111_000; let (mut pq, tester) = pq_test_setup(
cfg,
time,
vec![
fake_response(status_codes::ACCEPTED, time, Some("1234")),
fake_response(status_codes::ACCEPTED, time, Some("1234")),
fake_response(status_codes::ACCEPTED, time + 100_000, Some("1234")),
],
);
pq.enqueue(&make_record(100)).unwrap();
pq.enqueue(&make_record(100)).unwrap();
pq.enqueue(&make_record(100)).unwrap(); // POST
pq.enqueue(&make_record(100)).unwrap();
pq.enqueue(&make_record(100)).unwrap();
pq.enqueue(&make_record(100)).unwrap(); // POST
pq.enqueue(&make_record(100)).unwrap();
pq.flush(true).unwrap(); // COMMIT
let t = tester.borrow();
assert!(t.cur_batch.is_none());
assert_eq!(t.all_posts.len(), 3);
assert_eq!(t.batches.len(), 1);
assert_eq!(t.batches[0].posts.len(), 3);
assert_eq!(t.batches[0].records, 7);
assert_eq!(t.batches[0].bytes, 700);
#[test] #[allow(clippy::cognitive_complexity)] fn test_pq_multi_post_multi_batch_bytes() { let cfg = InfoConfiguration {
max_post_bytes: 300,
max_total_bytes: 500,
..InfoConfiguration::default()
}; let time = 11_111_111_000; let (mut pq, tester) = pq_test_setup(
cfg,
time,
vec![
fake_response(status_codes::ACCEPTED, time, Some("1234")),
fake_response(status_codes::ACCEPTED, time + 100_000, Some("1234")), // should commit
fake_response(status_codes::ACCEPTED, time + 100_000, Some("abcd")),
fake_response(status_codes::ACCEPTED, time + 200_000, Some("abcd")), // should commit
],
);
pq.enqueue(&make_record(100)).unwrap();
pq.enqueue(&make_record(100)).unwrap();
pq.enqueue(&make_record(100)).unwrap();
assert_eq!(pq.last_modified.0, time); // POST
pq.enqueue(&make_record(100)).unwrap();
pq.enqueue(&make_record(100)).unwrap(); // POST + COMMIT
pq.enqueue(&make_record(100)).unwrap();
assert_eq!(pq.last_modified.0, time + 100_000);
pq.enqueue(&make_record(100)).unwrap();
pq.enqueue(&make_record(100)).unwrap();
// POST
pq.enqueue(&make_record(100)).unwrap();
assert_eq!(pq.last_modified.0, time + 100_000);
pq.flush(true).unwrap(); // COMMIT
assert_eq!(pq.last_modified.0, time + 200_000);
let t = tester.borrow();
assert!(t.cur_batch.is_none());
assert_eq!(t.all_posts.len(), 4);
assert_eq!(t.batches.len(), 2);
assert_eq!(t.batches[0].posts.len(), 2);
assert_eq!(t.batches[1].posts.len(), 2);
// TODO: Test // // - error cases!!! We don't test our handling of server errors at all! // - mixed bytes/record limits // // A lot of these have good examples in test_postqueue.js on deskftop sync
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.17 Sekunden
(vorverarbeitet am 2026-06-20)
¤
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.