From 087333a227dab1ba28cdab6bb32ffea9031cbcbc Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 25 Nov 2020 23:16:05 -0500 Subject: [PATCH 1/9] refactor sync server into modules --- Cargo.lock | 1388 +++++++++++++++++++++- sync-server/Cargo.toml | 5 + sync-server/src/api/add_version.rs | 25 + sync-server/src/api/get_child_version.rs | 19 + sync-server/src/api/mod.rs | 2 + sync-server/src/lib.rs | 86 -- sync-server/src/main.rs | 24 + sync-server/src/server.rs | 34 + sync-server/src/types.rs | 23 + 9 files changed, 1504 insertions(+), 102 deletions(-) create mode 100644 sync-server/src/api/add_version.rs create mode 100644 sync-server/src/api/get_child_version.rs create mode 100644 sync-server/src/api/mod.rs delete mode 100644 sync-server/src/lib.rs create mode 100644 sync-server/src/main.rs create mode 100644 sync-server/src/server.rs create mode 100644 sync-server/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index c5ee93f04..6f24b0e1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,266 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "actix-codec" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project 0.4.27", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-connect" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177837a10863f15ba8d3ae3ec12fac1099099529ed20083a27fdfe247381d0dc" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "derive_more", + "either", + "futures-util", + "http", + "log", + "trust-dns-proto", + "trust-dns-resolver", +] + +[[package]] +name = "actix-http" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "452299e87817ae5673910e53c243484ca38be3828db819b6011736fc6982e874" +dependencies = [ + "actix-codec", + "actix-connect", + "actix-rt", + "actix-service", + "actix-threadpool", + "actix-utils", + "base64 0.13.0", + "bitflags", + "brotli2", + "bytes", + "cookie", + "copyless", + "derive_more", + "either", + "encoding_rs", + "flate2", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "h2", + "http", + "httparse", + "indexmap", + "itoa", + "language-tags", + "lazy_static", + "log", + "mime", + "percent-encoding", + "pin-project 1.0.2", + "rand 0.7.3", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "sha-1", + "slab", + "time 0.2.23", +] + +[[package]] +name = "actix-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a60f9ba7c4e6df97f3aacb14bb5c0cd7d98a49dcbaed0d7f292912ad9a6a3ed2" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd1f7dbda1645bf7da33554db60891755f6c01c1b2169e2f4c492098d30c235" +dependencies = [ + "bytestring", + "http", + "log", + "regex", + "serde", +] + +[[package]] +name = "actix-rt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227" +dependencies = [ + "actix-macros", + "actix-threadpool", + "copyless", + "futures-channel", + "futures-util", + "smallvec", + "tokio", +] + +[[package]] +name = "actix-server" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45407e6e672ca24784baa667c5d32ef109ccdd8d5e0b5ebb9ef8a67f4dfb708e" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "futures-channel", + "futures-util", + "log", + "mio", + "mio-uds", + "num_cpus", + "slab", + "socket2", +] + +[[package]] +name = "actix-service" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0052435d581b5be835d11f4eb3bce417c8af18d87ddf8ace99f8e67e595882bb" +dependencies = [ + "futures-util", + "pin-project 0.4.27", +] + +[[package]] +name = "actix-testing" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47239ca38799ab74ee6a8a94d1ce857014b2ac36f242f70f3f75a66f691e791c" +dependencies = [ + "actix-macros", + "actix-rt", + "actix-server", + "actix-service", + "log", + "socket2", +] + +[[package]] +name = "actix-threadpool" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d209f04d002854b9afd3743032a27b066158817965bf5d036824d19ac2cc0e30" +dependencies = [ + "derive_more", + "futures-channel", + "lazy_static", + "log", + "num_cpus", + "parking_lot", + "threadpool", +] + +[[package]] +name = "actix-tls" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24789b7d7361cf5503a504ebe1c10806896f61e96eca9a7350e23001aca715fb" +dependencies = [ + "actix-codec", + "actix-service", + "actix-utils", + "futures-util", +] + +[[package]] +name = "actix-utils" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9022dec56632d1d7979e59af14f0597a28a830a9c1c7fec8b2327eb9f16b5a" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "bitflags", + "bytes", + "either", + "futures-channel", + "futures-sink", + "futures-util", + "log", + "pin-project 0.4.27", + "slab", +] + +[[package]] +name = "actix-web" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a89a7b133e734f6d1e555502d450408ae04105826aef7e3605019747d3ac732" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-testing", + "actix-threadpool", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "awc", + "bytes", + "derive_more", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "log", + "mime", + "pin-project 1.0.2", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "socket2", + "time 0.2.23", + "tinyvec", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "addr2line" version = "0.14.0" @@ -30,7 +291,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -58,6 +319,17 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-trait" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -66,7 +338,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -81,6 +353,30 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "awc" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9056f5e27b0d56bedd82f78eceaba0bcddcbbcbbefae3cd0a53994b28c96ff5" +dependencies = [ + "actix-codec", + "actix-http", + "actix-rt", + "actix-service", + "base64 0.13.0", + "bytes", + "cfg-if 1.0.0", + "derive_more", + "futures-core", + "log", + "mime", + "percent-encoding", + "rand 0.7.3", + "serde", + "serde_json", + "serde_urlencoded", +] + [[package]] name = "backtrace" version = "0.3.55" @@ -95,12 +391,24 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + [[package]] name = "base64" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bit-set" version = "0.5.2" @@ -133,6 +441,35 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "brotli2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" +dependencies = [ + "brotli-sys", + "libc", +] + [[package]] name = "bstr" version = "0.2.14" @@ -157,6 +494,21 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytestring" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363" +dependencies = [ + "bytes", +] + [[package]] name = "cc" version = "1.0.65" @@ -185,8 +537,8 @@ dependencies = [ "num-integer", "num-traits", "serde", - "time", - "winapi", + "time 0.1.44", + "winapi 0.3.9", ] [[package]] @@ -213,12 +565,59 @@ dependencies = [ "bitflags", ] +[[package]] +name = "cloudabi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" +dependencies = [ + "bitflags", +] + +[[package]] +name = "const_fn" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab" + [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "cookie" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" +dependencies = [ + "percent-encoding", + "time 0.2.23", + "version_check", +] + +[[package]] +name = "copyless" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "crossbeam-utils" version = "0.7.2" @@ -252,12 +651,32 @@ dependencies = [ "memchr", ] +[[package]] +name = "derive_more" +version = "0.99.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "difference" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "dirs" version = "1.0.5" @@ -266,21 +685,54 @@ checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "enum-as-inner" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "failure" version = "0.1.8" @@ -303,6 +755,18 @@ dependencies = [ "synstructure", ] +[[package]] +name = "flate2" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + [[package]] name = "float-cmp" version = "0.8.0" @@ -318,12 +782,140 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" + +[[package]] +name = "futures-io" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb" + +[[package]] +name = "futures-macro" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" + +[[package]] +name = "futures-task" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project 1.0.2", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.1.15" @@ -341,6 +933,41 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", + "tracing-futures", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.17" @@ -350,6 +977,85 @@ dependencies = [ "libc", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi 0.3.9", +] + +[[package]] +name = "http" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +dependencies = [ + "autocfg 1.0.1", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipconfig" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" +dependencies = [ + "socket2", + "widestring", + "winapi 0.3.9", + "winreg", +] + [[package]] name = "itoa" version = "0.4.6" @@ -365,6 +1071,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "kv" version = "0.10.0" @@ -378,6 +1094,12 @@ dependencies = [ "toml", ] +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + [[package]] name = "lazy_static" version = "1.4.0" @@ -390,6 +1112,12 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + [[package]] name = "lmdb-rkv" version = "0.12.3" @@ -413,6 +1141,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "lock_api" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.11" @@ -422,12 +1159,39 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + [[package]] name = "memchr" version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "miniz_oxide" version = "0.4.3" @@ -438,6 +1202,59 @@ dependencies = [ "autocfg 1.0.1", ] +[[package]] +name = "mio" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "net2" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ebc3ec692ed7c9a255596c67808dee269f64655d8baf7b4f0638e51ba1d6853" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -463,6 +1280,16 @@ dependencies = [ "autocfg 1.0.1", ] +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.22.0" @@ -475,6 +1302,102 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi 0.1.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" +dependencies = [ + "pin-project-internal 0.4.27", +] + +[[package]] +name = "pin-project" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccc2237c2c489783abd8c4c80e5450fc0e98644555b1364da68cc29aa151ca7" +dependencies = [ + "pin-project-internal 1.0.2", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" + +[[package]] +name = "pin-project-lite" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.19" @@ -530,6 +1453,18 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" + [[package]] name = "proc-macro2" version = "1.0.24" @@ -584,7 +1519,7 @@ dependencies = [ "libc", "rand_core 0.3.1", "rdrand", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -603,7 +1538,7 @@ dependencies = [ "rand_os", "rand_pcg", "rand_xorshift", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -698,7 +1633,7 @@ checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" dependencies = [ "libc", "rand_core 0.4.2", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -707,12 +1642,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" dependencies = [ - "cloudabi", + "cloudabi 0.0.3", "fuchsia-cprng", "libc", "rand_core 0.4.2", "rdrand", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -793,7 +1728,17 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", ] [[package]] @@ -808,7 +1753,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -838,7 +1783,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" dependencies = [ - "base64", + "base64 0.12.3", "blake2b_simd", "constant_time_eq", "crossbeam-utils", @@ -850,6 +1795,15 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + [[package]] name = "rusty-fork" version = "0.2.2" @@ -868,6 +1822,27 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.117" @@ -899,12 +1874,134 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool", + "digest", + "opaque-debug", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "signal-hook-registry" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85" + +[[package]] +name = "socket2" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "standback" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf906c8b8fc3f6ecd1046e01da1d8ddec83e48c8b08b84dcc02b585a6bedf5a8" +dependencies = [ + "version_check", +] + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + [[package]] name = "strsim" version = "0.8.0" @@ -925,6 +2022,13 @@ dependencies = [ [[package]] name = "sync-server" version = "0.1.0" +dependencies = [ + "actix-web", + "failure", + "serde", + "serde_json", + "taskchampion", +] [[package]] name = "synstructure" @@ -987,7 +2091,7 @@ dependencies = [ "rand 0.7.3", "redox_syscall", "remove_dir_all", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -998,7 +2102,7 @@ checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" dependencies = [ "byteorder", "dirs", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1039,6 +2143,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "time" version = "0.1.44" @@ -1047,7 +2160,94 @@ checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcdaeea317915d59b2b4cd3b5efcd156c309108664277793f5351700c02ce98b" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check", + "winapi 0.3.9", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff" +dependencies = [ + "bytes", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-uds", + "pin-project-lite 0.1.11", + "signal-hook-registry", + "slab", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.11", + "tokio", ] [[package]] @@ -1059,12 +2259,113 @@ dependencies = [ "serde", ] +[[package]] +name = "tracing" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite 0.2.0", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-futures" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +dependencies = [ + "pin-project 0.4.27", + "tracing", +] + [[package]] name = "treeline" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" +[[package]] +name = "trust-dns-proto" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53861fcb288a166aae4c508ae558ed18b53838db728d4d310aad08270a7d4c2b" +dependencies = [ + "async-trait", + "backtrace", + "enum-as-inner", + "futures", + "idna", + "lazy_static", + "log", + "rand 0.7.3", + "smallvec", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6759e8efc40465547b0dfce9500d733c65f969a4cbbfbe3ccf68daaa46ef179e" +dependencies = [ + "backtrace", + "cfg-if 0.1.10", + "futures", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + [[package]] name = "unicode-width" version = "0.1.8" @@ -1083,6 +2384,18 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "uuid" version = "0.8.1" @@ -1099,6 +2412,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + [[package]] name = "wait-timeout" version = "0.2.0" @@ -1184,6 +2503,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -1194,6 +2525,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -1205,3 +2542,22 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] diff --git a/sync-server/Cargo.toml b/sync-server/Cargo.toml index e2df075ed..d243e2647 100644 --- a/sync-server/Cargo.toml +++ b/sync-server/Cargo.toml @@ -7,3 +7,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +actix-web = "3.3.0" +failure = "0.1.8" +serde = "1.0.117" +serde_json = "1.0.59" +taskchampion = { path = "../taskchampion" } diff --git a/sync-server/src/api/add_version.rs b/sync-server/src/api/add_version.rs new file mode 100644 index 000000000..75398ecd9 --- /dev/null +++ b/sync-server/src/api/add_version.rs @@ -0,0 +1,25 @@ +use crate::server::SyncServer; +use crate::types::{ClientId, HistorySegment, VersionId}; +use actix_web::{error, http::StatusCode, post, web, HttpResponse, Responder, Result}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +/// Request body to add_version +#[derive(Serialize, Deserialize)] +pub(crate) struct AddVersionRequest { + // TODO: temporary! + #[serde(default)] + history_segment: HistorySegment, +} + +#[post("/client/{client_id}/add-version/{parent_version_id}")] +pub(crate) async fn service( + data: web::Data>, + web::Path((client_id, parent_version_id)): web::Path<(ClientId, VersionId)>, + body: web::Json, +) -> Result { + let result = data + .add_version(client_id, parent_version_id, &body.history_segment) + .map_err(|e| error::InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?; + Ok(HttpResponse::Ok().json(result)) +} diff --git a/sync-server/src/api/get_child_version.rs b/sync-server/src/api/get_child_version.rs new file mode 100644 index 000000000..8512e6f4a --- /dev/null +++ b/sync-server/src/api/get_child_version.rs @@ -0,0 +1,19 @@ +use crate::server::SyncServer; +use crate::types::{ClientId, VersionId}; +use actix_web::{error, get, http::StatusCode, web, HttpResponse, Result}; +use std::sync::Arc; + +#[get("/client/{client_id}/get-child-version/{parent_version_id}")] +pub(crate) async fn service( + data: web::Data>, + web::Path((client_id, parent_version_id)): web::Path<(ClientId, VersionId)>, +) -> Result { + let result = data + .get_child_version(client_id, parent_version_id) + .map_err(|e| error::InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?; + if let Some(result) = result { + Ok(HttpResponse::Ok().json(result)) + } else { + Err(error::ErrorNotFound("no such version")) + } +} diff --git a/sync-server/src/api/mod.rs b/sync-server/src/api/mod.rs new file mode 100644 index 000000000..b955fcc25 --- /dev/null +++ b/sync-server/src/api/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod add_version; +pub(crate) mod get_child_version; diff --git a/sync-server/src/lib.rs b/sync-server/src/lib.rs deleted file mode 100644 index e85125ed0..000000000 --- a/sync-server/src/lib.rs +++ /dev/null @@ -1,86 +0,0 @@ -#![allow(clippy::new_without_default)] - -use std::collections::HashMap; - -type Blob = Vec; - -struct User { - // versions, indexed at v-1 - versions: Vec, - snapshots: HashMap, -} - -pub struct Server { - users: HashMap, -} - -pub enum VersionAdd { - // OK, version added - Ok, - // Rejected, must be based on the the given version - ExpectedVersion(u64), -} - -impl User { - fn new() -> User { - User { - versions: vec![], - snapshots: HashMap::new(), - } - } - - fn get_versions(&self, since_version: u64) -> Vec { - let last_version = self.versions.len(); - if last_version == since_version as usize { - return vec![]; - } - self.versions[since_version as usize..last_version].to_vec() - } - - fn add_version(&mut self, version: u64, blob: Blob) -> VersionAdd { - // of by one here: client wants to send version 1 first - let expected_version = self.versions.len() as u64 + 1; - if version != expected_version { - return VersionAdd::ExpectedVersion(expected_version); - } - self.versions.push(blob); - - VersionAdd::Ok - } - - fn add_snapshot(&mut self, version: u64, blob: Blob) { - self.snapshots.insert(version, blob); - } -} - -impl Server { - pub fn new() -> Server { - Server { - users: HashMap::new(), - } - } - - fn get_user_mut(&mut self, username: &str) -> &mut User { - self.users - .entry(username.to_string()) - .or_insert_with(User::new) - } - - /// Get a vector of all versions after `since_version` - pub fn get_versions(&self, username: &str, since_version: u64) -> Vec { - self.users - .get(username) - .map(|user| user.get_versions(since_version)) - .unwrap_or_default() - } - - /// Add a new version. If the given version number is incorrect, this responds with the - /// appropriate version and expects the caller to try again. - pub fn add_version(&mut self, username: &str, version: u64, blob: Blob) -> VersionAdd { - self.get_user_mut(username).add_version(version, blob) - } - - pub fn add_snapshot(&mut self, username: &str, version: u64, blob: Blob) { - self.get_user_mut(username).add_snapshot(version, blob); - } -} diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs new file mode 100644 index 000000000..20b7b118f --- /dev/null +++ b/sync-server/src/main.rs @@ -0,0 +1,24 @@ +use actix_web::{App, HttpServer}; +use server::SyncServer; +use std::sync::Arc; + +mod api; +mod server; +mod types; + +// TODO: use hawk to sign requests + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let sync_server = Arc::new(SyncServer::new()); + + HttpServer::new(move || { + App::new() + .data(sync_server.clone()) + .service(api::get_child_version::service) + .service(api::add_version::service) + }) + .bind("127.0.0.1:8080")? + .run() + .await +} diff --git a/sync-server/src/server.rs b/sync-server/src/server.rs new file mode 100644 index 000000000..8c4bdfdf2 --- /dev/null +++ b/sync-server/src/server.rs @@ -0,0 +1,34 @@ +use crate::types::{AddVersionResult, ClientId, GetVersionResult, HistorySegment, VersionId}; +use failure::Fallible; +use taskchampion::Uuid; + +/// The sync server's implementation; HTTP API method call through to methods on a single +/// instance of this type. +pub(crate) struct SyncServer {} + +impl SyncServer { + pub(crate) fn new() -> Self { + Self {} + } + + pub(crate) fn get_child_version( + &self, + _client_id: ClientId, + parent_version_id: VersionId, + ) -> Fallible> { + Ok(Some(GetVersionResult { + version_id: Uuid::new_v4(), + parent_version_id, + history_segment: b"abcd".to_vec(), + })) + } + + pub(crate) fn add_version( + &self, + _client_id: ClientId, + _parent_version_id: VersionId, + _history_segment: &HistorySegment, + ) -> Fallible { + Ok(AddVersionResult::Ok(Uuid::new_v4())) + } +} diff --git a/sync-server/src/types.rs b/sync-server/src/types.rs new file mode 100644 index 000000000..f4b28901d --- /dev/null +++ b/sync-server/src/types.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use taskchampion::Uuid; + +pub(crate) type HistorySegment = Vec; +pub(crate) type ClientId = Uuid; +pub(crate) type VersionId = Uuid; + +/// Response to get_child_version +#[derive(Serialize, Deserialize)] +pub(crate) struct GetVersionResult { + pub(crate) version_id: Uuid, + pub(crate) parent_version_id: Uuid, + pub(crate) history_segment: HistorySegment, +} + +/// Response to add_version +#[derive(Serialize, Deserialize)] +pub(crate) enum AddVersionResult { + /// OK, version added with the given ID + Ok(VersionId), + /// Rejected; expected a version with the given parent version + ExpectedParentVersion(VersionId), +} From a5c06008b3894ef28ddf86c25e2a7a35f475f537 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 26 Nov 2020 10:52:28 -0500 Subject: [PATCH 2/9] use a trait object for the server, for dynamic dispatch --- sync-server/src/api/add_version.rs | 5 ++-- sync-server/src/api/get_child_version.rs | 5 ++-- sync-server/src/api/mod.rs | 6 ++++ sync-server/src/main.rs | 5 ++-- sync-server/src/{server.rs => server/mod.rs} | 30 ++++++++++++++++---- 5 files changed, 36 insertions(+), 15 deletions(-) rename sync-server/src/{server.rs => server/mod.rs} (54%) diff --git a/sync-server/src/api/add_version.rs b/sync-server/src/api/add_version.rs index 75398ecd9..cdc96db2a 100644 --- a/sync-server/src/api/add_version.rs +++ b/sync-server/src/api/add_version.rs @@ -1,8 +1,7 @@ -use crate::server::SyncServer; +use crate::api::ServerState; use crate::types::{ClientId, HistorySegment, VersionId}; use actix_web::{error, http::StatusCode, post, web, HttpResponse, Responder, Result}; use serde::{Deserialize, Serialize}; -use std::sync::Arc; /// Request body to add_version #[derive(Serialize, Deserialize)] @@ -14,7 +13,7 @@ pub(crate) struct AddVersionRequest { #[post("/client/{client_id}/add-version/{parent_version_id}")] pub(crate) async fn service( - data: web::Data>, + data: web::Data, web::Path((client_id, parent_version_id)): web::Path<(ClientId, VersionId)>, body: web::Json, ) -> Result { diff --git a/sync-server/src/api/get_child_version.rs b/sync-server/src/api/get_child_version.rs index 8512e6f4a..4383a6fbc 100644 --- a/sync-server/src/api/get_child_version.rs +++ b/sync-server/src/api/get_child_version.rs @@ -1,11 +1,10 @@ -use crate::server::SyncServer; +use crate::api::ServerState; use crate::types::{ClientId, VersionId}; use actix_web::{error, get, http::StatusCode, web, HttpResponse, Result}; -use std::sync::Arc; #[get("/client/{client_id}/get-child-version/{parent_version_id}")] pub(crate) async fn service( - data: web::Data>, + data: web::Data, web::Path((client_id, parent_version_id)): web::Path<(ClientId, VersionId)>, ) -> Result { let result = data diff --git a/sync-server/src/api/mod.rs b/sync-server/src/api/mod.rs index b955fcc25..cdab25fca 100644 --- a/sync-server/src/api/mod.rs +++ b/sync-server/src/api/mod.rs @@ -1,2 +1,8 @@ +use crate::server::SyncServer; +use std::sync::Arc; + pub(crate) mod add_version; pub(crate) mod get_child_version; + +/// The type containing a reference to the SyncServer object in the Actix state. +pub(crate) type ServerState = Arc>; diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs index 20b7b118f..191908fb2 100644 --- a/sync-server/src/main.rs +++ b/sync-server/src/main.rs @@ -1,5 +1,4 @@ use actix_web::{App, HttpServer}; -use server::SyncServer; use std::sync::Arc; mod api; @@ -10,11 +9,11 @@ mod types; #[actix_web::main] async fn main() -> std::io::Result<()> { - let sync_server = Arc::new(SyncServer::new()); + let server_state = Arc::new(Box::new(server::NullSyncServer::new())); HttpServer::new(move || { App::new() - .data(sync_server.clone()) + .data(server_state.clone()) .service(api::get_child_version::service) .service(api::add_version::service) }) diff --git a/sync-server/src/server.rs b/sync-server/src/server/mod.rs similarity index 54% rename from sync-server/src/server.rs rename to sync-server/src/server/mod.rs index 8c4bdfdf2..df4377366 100644 --- a/sync-server/src/server.rs +++ b/sync-server/src/server/mod.rs @@ -2,16 +2,34 @@ use crate::types::{AddVersionResult, ClientId, GetVersionResult, HistorySegment, use failure::Fallible; use taskchampion::Uuid; -/// The sync server's implementation; HTTP API method call through to methods on a single -/// instance of this type. -pub(crate) struct SyncServer {} +pub(crate) trait SyncServer { + fn get_child_version( + &self, + client_id: ClientId, + parent_version_id: VersionId, + ) -> Fallible>; -impl SyncServer { + fn add_version( + &self, + client_id: ClientId, + parent_version_id: VersionId, + history_segment: &HistorySegment, + ) -> Fallible; +} + +// TODO: temporary +/// A "null" sync server's implementation; HTTP API methods call through to methods on a single +/// instance of this type. +pub(crate) struct NullSyncServer {} + +impl NullSyncServer { pub(crate) fn new() -> Self { Self {} } +} - pub(crate) fn get_child_version( +impl SyncServer for NullSyncServer { + fn get_child_version( &self, _client_id: ClientId, parent_version_id: VersionId, @@ -23,7 +41,7 @@ impl SyncServer { })) } - pub(crate) fn add_version( + fn add_version( &self, _client_id: ClientId, _parent_version_id: VersionId, From e84871931f068a33b1d3e16eb43fd1e0163c7ba7 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 26 Nov 2020 11:32:20 -0500 Subject: [PATCH 3/9] Refactor HTTP implementation of API methods --- Cargo.lock | 15 +++++- sync-server/Cargo.toml | 3 +- sync-server/src/api/add_version.rs | 63 ++++++++++++++++++------ sync-server/src/api/get_child_version.rs | 21 +++++++- sync-server/src/api/mod.rs | 10 ++++ sync-server/src/main.rs | 6 ++- sync-server/src/server/mod.rs | 9 ++-- sync-server/src/types.rs | 3 -- 8 files changed, 100 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f24b0e1f..4c744d952 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -822,6 +822,7 @@ checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -844,6 +845,17 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" +[[package]] +name = "futures-executor" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4caa2b2b68b880003057c1dd49f1ed937e38f22fcf6c212188a121f08cf40a65" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.8" @@ -2025,8 +2037,7 @@ version = "0.1.0" dependencies = [ "actix-web", "failure", - "serde", - "serde_json", + "futures", "taskchampion", ] diff --git a/sync-server/Cargo.toml b/sync-server/Cargo.toml index d243e2647..3c638aa7c 100644 --- a/sync-server/Cargo.toml +++ b/sync-server/Cargo.toml @@ -9,6 +9,5 @@ edition = "2018" [dependencies] actix-web = "3.3.0" failure = "0.1.8" -serde = "1.0.117" -serde_json = "1.0.59" +futures = "0.3.8" taskchampion = { path = "../taskchampion" } diff --git a/sync-server/src/api/add_version.rs b/sync-server/src/api/add_version.rs index cdc96db2a..40cb5fdd0 100644 --- a/sync-server/src/api/add_version.rs +++ b/sync-server/src/api/add_version.rs @@ -1,24 +1,57 @@ -use crate::api::ServerState; -use crate::types::{ClientId, HistorySegment, VersionId}; -use actix_web::{error, http::StatusCode, post, web, HttpResponse, Responder, Result}; -use serde::{Deserialize, Serialize}; +use crate::api::{ + ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER, +}; +use crate::types::{AddVersionResult, ClientId, VersionId}; +use actix_web::{ + error, http::StatusCode, post, web, HttpMessage, HttpRequest, HttpResponse, Result, +}; +use futures::StreamExt; -/// Request body to add_version -#[derive(Serialize, Deserialize)] -pub(crate) struct AddVersionRequest { - // TODO: temporary! - #[serde(default)] - history_segment: HistorySegment, -} +/// Max history segment size: 100MB +const MAX_SIZE: usize = 100 * 1024 * 1024; +/// Add a new version, after checking prerequisites. The history segment should be transmitted in +/// the request entity body and must have content-type +/// `application/vnd.taskchampion.history-segment`. The content can be encoded in any of the +/// formats supported by actix-web. +/// +/// On success, the response is a 200 OK with the new version ID in the `X-Version-Id` header. If +/// the version cannot be added due to a conflict, the response is a 409 CONFLICT with the expected +/// parent version ID in the `X-Parent-Version-Id` header. +/// +/// Returns other 4xx or 5xx responses on other errors. #[post("/client/{client_id}/add-version/{parent_version_id}")] pub(crate) async fn service( + req: HttpRequest, data: web::Data, web::Path((client_id, parent_version_id)): web::Path<(ClientId, VersionId)>, - body: web::Json, -) -> Result { + mut payload: web::Payload, +) -> Result { + // check content-type + if req.content_type() != HISTORY_SEGMENT_CONTENT_TYPE { + return Err(error::ErrorBadRequest("Bad content-type")); + } + + // read the body in its entirety + let mut body = web::BytesMut::new(); + while let Some(chunk) = payload.next().await { + let chunk = chunk?; + // limit max size of in-memory payload + if (body.len() + chunk.len()) > MAX_SIZE { + return Err(error::ErrorBadRequest("overflow")); + } + body.extend_from_slice(&chunk); + } + let result = data - .add_version(client_id, parent_version_id, &body.history_segment) + .add_version(client_id, parent_version_id, body.to_vec()) .map_err(|e| error::InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?; - Ok(HttpResponse::Ok().json(result)) + Ok(match result { + AddVersionResult::Ok(version_id) => HttpResponse::Ok() + .header(VERSION_ID_HEADER, version_id.to_string()) + .body(""), + AddVersionResult::ExpectedParentVersion(parent_version_id) => HttpResponse::Conflict() + .header(PARENT_VERSION_ID_HEADER, parent_version_id.to_string()) + .body(""), + }) } diff --git a/sync-server/src/api/get_child_version.rs b/sync-server/src/api/get_child_version.rs index 4383a6fbc..0667ec034 100644 --- a/sync-server/src/api/get_child_version.rs +++ b/sync-server/src/api/get_child_version.rs @@ -1,7 +1,17 @@ -use crate::api::ServerState; +use crate::api::{ + ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER, +}; use crate::types::{ClientId, VersionId}; use actix_web::{error, get, http::StatusCode, web, HttpResponse, Result}; +/// Get a child version. +/// +/// On succcess, the response is the same sequence of bytes originally sent to the server, +/// with content-type `application/vnd.taskchampion.history-segment`. The `X-Version-Id` and +/// `X-Parent-Version-Id` headers contain the corresponding values. +/// +/// If no such child exists, returns a 404 with no content. +/// Returns other 4xx or 5xx responses on other errors. #[get("/client/{client_id}/get-child-version/{parent_version_id}")] pub(crate) async fn service( data: web::Data, @@ -11,7 +21,14 @@ pub(crate) async fn service( .get_child_version(client_id, parent_version_id) .map_err(|e| error::InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?; if let Some(result) = result { - Ok(HttpResponse::Ok().json(result)) + Ok(HttpResponse::Ok() + .content_type(HISTORY_SEGMENT_CONTENT_TYPE) + .header(VERSION_ID_HEADER, result.version_id.to_string()) + .header( + PARENT_VERSION_ID_HEADER, + result.parent_version_id.to_string(), + ) + .body(result.history_segment)) } else { Err(error::ErrorNotFound("no such version")) } diff --git a/sync-server/src/api/mod.rs b/sync-server/src/api/mod.rs index cdab25fca..9dcca18bc 100644 --- a/sync-server/src/api/mod.rs +++ b/sync-server/src/api/mod.rs @@ -4,5 +4,15 @@ use std::sync::Arc; pub(crate) mod add_version; pub(crate) mod get_child_version; +/// The content-type for history segments (opaque blobs of bytes) +pub(crate) const HISTORY_SEGMENT_CONTENT_TYPE: &str = + "application/vnd.taskchampion.history-segment"; + +/// The header names for version ID +pub(crate) const VERSION_ID_HEADER: &str = "X-Version-Id"; + +/// The header names for parent version ID +pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id"; + /// The type containing a reference to the SyncServer object in the Actix state. pub(crate) type ServerState = Arc>; diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs index 191908fb2..7caa360f8 100644 --- a/sync-server/src/main.rs +++ b/sync-server/src/main.rs @@ -1,5 +1,6 @@ use actix_web::{App, HttpServer}; -use std::sync::Arc; +use api::ServerState; +use server::{NullSyncServer, SyncServer}; mod api; mod server; @@ -9,7 +10,8 @@ mod types; #[actix_web::main] async fn main() -> std::io::Result<()> { - let server_state = Arc::new(Box::new(server::NullSyncServer::new())); + let server_box: Box = Box::new(NullSyncServer::new()); + let server_state = ServerState::new(server_box); HttpServer::new(move || { App::new() diff --git a/sync-server/src/server/mod.rs b/sync-server/src/server/mod.rs index df4377366..71961468b 100644 --- a/sync-server/src/server/mod.rs +++ b/sync-server/src/server/mod.rs @@ -2,7 +2,7 @@ use crate::types::{AddVersionResult, ClientId, GetVersionResult, HistorySegment, use failure::Fallible; use taskchampion::Uuid; -pub(crate) trait SyncServer { +pub(crate) trait SyncServer: Sync + Send { fn get_child_version( &self, client_id: ClientId, @@ -13,7 +13,7 @@ pub(crate) trait SyncServer { &self, client_id: ClientId, parent_version_id: VersionId, - history_segment: &HistorySegment, + history_segment: HistorySegment, ) -> Fallible; } @@ -45,8 +45,9 @@ impl SyncServer for NullSyncServer { &self, _client_id: ClientId, _parent_version_id: VersionId, - _history_segment: &HistorySegment, + _history_segment: HistorySegment, ) -> Fallible { - Ok(AddVersionResult::Ok(Uuid::new_v4())) + //Ok(AddVersionResult::Ok(Uuid::new_v4())) + Ok(AddVersionResult::ExpectedParentVersion(Uuid::new_v4())) } } diff --git a/sync-server/src/types.rs b/sync-server/src/types.rs index f4b28901d..69dbe2fbc 100644 --- a/sync-server/src/types.rs +++ b/sync-server/src/types.rs @@ -1,4 +1,3 @@ -use serde::{Deserialize, Serialize}; use taskchampion::Uuid; pub(crate) type HistorySegment = Vec; @@ -6,7 +5,6 @@ pub(crate) type ClientId = Uuid; pub(crate) type VersionId = Uuid; /// Response to get_child_version -#[derive(Serialize, Deserialize)] pub(crate) struct GetVersionResult { pub(crate) version_id: Uuid, pub(crate) parent_version_id: Uuid, @@ -14,7 +12,6 @@ pub(crate) struct GetVersionResult { } /// Response to add_version -#[derive(Serialize, Deserialize)] pub(crate) enum AddVersionResult { /// OK, version added with the given ID Ok(VersionId), From 2457d8bc435079f964eea1c26148f96d4c0e24fe Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 26 Nov 2020 11:34:04 -0500 Subject: [PATCH 4/9] move types in crate::types to crate::server --- sync-server/src/api/add_version.rs | 2 +- sync-server/src/api/get_child_version.rs | 2 +- sync-server/src/main.rs | 1 - sync-server/src/server/mod.rs | 19 ++++++++++++++++++- sync-server/src/types.rs | 20 -------------------- 5 files changed, 20 insertions(+), 24 deletions(-) delete mode 100644 sync-server/src/types.rs diff --git a/sync-server/src/api/add_version.rs b/sync-server/src/api/add_version.rs index 40cb5fdd0..83f010ef5 100644 --- a/sync-server/src/api/add_version.rs +++ b/sync-server/src/api/add_version.rs @@ -1,7 +1,7 @@ use crate::api::{ ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER, }; -use crate::types::{AddVersionResult, ClientId, VersionId}; +use crate::server::{AddVersionResult, ClientId, VersionId}; use actix_web::{ error, http::StatusCode, post, web, HttpMessage, HttpRequest, HttpResponse, Result, }; diff --git a/sync-server/src/api/get_child_version.rs b/sync-server/src/api/get_child_version.rs index 0667ec034..4056408ce 100644 --- a/sync-server/src/api/get_child_version.rs +++ b/sync-server/src/api/get_child_version.rs @@ -1,7 +1,7 @@ use crate::api::{ ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER, }; -use crate::types::{ClientId, VersionId}; +use crate::server::{ClientId, VersionId}; use actix_web::{error, get, http::StatusCode, web, HttpResponse, Result}; /// Get a child version. diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs index 7caa360f8..880ce7b27 100644 --- a/sync-server/src/main.rs +++ b/sync-server/src/main.rs @@ -4,7 +4,6 @@ use server::{NullSyncServer, SyncServer}; mod api; mod server; -mod types; // TODO: use hawk to sign requests diff --git a/sync-server/src/server/mod.rs b/sync-server/src/server/mod.rs index 71961468b..5bda024f0 100644 --- a/sync-server/src/server/mod.rs +++ b/sync-server/src/server/mod.rs @@ -1,7 +1,24 @@ -use crate::types::{AddVersionResult, ClientId, GetVersionResult, HistorySegment, VersionId}; use failure::Fallible; use taskchampion::Uuid; +pub(crate) type HistorySegment = Vec; +pub(crate) type ClientId = Uuid; +pub(crate) type VersionId = Uuid; + +/// Response to get_child_version +pub(crate) struct GetVersionResult { + pub(crate) version_id: Uuid, + pub(crate) parent_version_id: Uuid, + pub(crate) history_segment: HistorySegment, +} + +/// Response to add_version +pub(crate) enum AddVersionResult { + /// OK, version added with the given ID + Ok(VersionId), + /// Rejected; expected a version with the given parent version + ExpectedParentVersion(VersionId), +} pub(crate) trait SyncServer: Sync + Send { fn get_child_version( &self, diff --git a/sync-server/src/types.rs b/sync-server/src/types.rs deleted file mode 100644 index 69dbe2fbc..000000000 --- a/sync-server/src/types.rs +++ /dev/null @@ -1,20 +0,0 @@ -use taskchampion::Uuid; - -pub(crate) type HistorySegment = Vec; -pub(crate) type ClientId = Uuid; -pub(crate) type VersionId = Uuid; - -/// Response to get_child_version -pub(crate) struct GetVersionResult { - pub(crate) version_id: Uuid, - pub(crate) parent_version_id: Uuid, - pub(crate) history_segment: HistorySegment, -} - -/// Response to add_version -pub(crate) enum AddVersionResult { - /// OK, version added with the given ID - Ok(VersionId), - /// Rejected; expected a version with the given parent version - ExpectedParentVersion(VersionId), -} From 2dae271851de9aed73e80a084d3388458c66d5a0 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 26 Nov 2020 12:13:00 -0500 Subject: [PATCH 5/9] build an in-memory sync server implementation --- sync-server/src/main.rs | 4 +- sync-server/src/server/inmemory.rs | 108 +++++++++++++++++++++++++++++ sync-server/src/server/mod.rs | 42 ++--------- 3 files changed, 117 insertions(+), 37 deletions(-) create mode 100644 sync-server/src/server/inmemory.rs diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs index 880ce7b27..a8e4cc9de 100644 --- a/sync-server/src/main.rs +++ b/sync-server/src/main.rs @@ -1,6 +1,6 @@ use actix_web::{App, HttpServer}; use api::ServerState; -use server::{NullSyncServer, SyncServer}; +use server::{InMemorySyncServer, SyncServer}; mod api; mod server; @@ -9,7 +9,7 @@ mod server; #[actix_web::main] async fn main() -> std::io::Result<()> { - let server_box: Box = Box::new(NullSyncServer::new()); + let server_box: Box = Box::new(InMemorySyncServer::new()); let server_state = ServerState::new(server_box); HttpServer::new(move || { diff --git a/sync-server/src/server/inmemory.rs b/sync-server/src/server/inmemory.rs new file mode 100644 index 000000000..37302282e --- /dev/null +++ b/sync-server/src/server/inmemory.rs @@ -0,0 +1,108 @@ +use super::{ + AddVersionResult, ClientId, GetVersionResult, HistorySegment, SyncServer, VersionId, + NO_VERSION_ID, +}; +use failure::Fallible; +use std::collections::HashMap; +use std::sync::{Mutex, RwLock}; +use taskchampion::Uuid; + +/// An in-memory server backend that can be useful for testing. +pub(crate) struct InMemorySyncServer { + clients: RwLock>>, +} + +struct Version { + version_id: VersionId, + history_segment: HistorySegment, +} + +struct Client { + latest_version_id: VersionId, + // NOTE: indexed by parent_version_id! + versions: HashMap, +} + +impl InMemorySyncServer { + pub(crate) fn new() -> Self { + Self { + clients: RwLock::new(HashMap::new()), + } + } +} + +impl SyncServer for InMemorySyncServer { + fn get_child_version( + &self, + client_id: ClientId, + parent_version_id: VersionId, + ) -> Fallible> { + let clients = self.clients.read().expect("poisoned lock"); + if let Some(client) = clients.get(&client_id) { + let client = client.lock().expect("poisoned lock"); + if let Some(version) = client.versions.get(&parent_version_id) { + return Ok(Some(GetVersionResult { + version_id: version.version_id, + parent_version_id, + history_segment: version.history_segment.clone(), + })); + } + } + Ok(None) + } + + fn add_version( + &self, + client_id: ClientId, + parent_version_id: VersionId, + history_segment: HistorySegment, + ) -> Fallible { + let mut clients = self.clients.write().expect("poisoned lock"); + if let Some(client) = clients.get_mut(&client_id) { + let mut client = client.lock().expect("poisoned lock"); + if client.latest_version_id != NO_VERSION_ID { + if parent_version_id != client.latest_version_id { + return Ok(AddVersionResult::ExpectedParentVersion( + client.latest_version_id, + )); + } + } + + // invent a new ID for this version + let version_id = Uuid::new_v4(); + + client.versions.insert( + parent_version_id, + Version { + version_id, + history_segment, + }, + ); + client.latest_version_id = version_id; + + Ok(AddVersionResult::Ok(version_id)) + } else { + // new client, so insert a client with just this new version + + let latest_version_id = Uuid::new_v4(); + let mut versions = HashMap::new(); + versions.insert( + parent_version_id, + Version { + version_id: latest_version_id, + history_segment, + }, + ); + + clients.insert( + client_id, + Mutex::new(Client { + latest_version_id, + versions, + }), + ); + + Ok(AddVersionResult::Ok(latest_version_id)) + } + } +} diff --git a/sync-server/src/server/mod.rs b/sync-server/src/server/mod.rs index 5bda024f0..6d8593c03 100644 --- a/sync-server/src/server/mod.rs +++ b/sync-server/src/server/mod.rs @@ -1,6 +1,13 @@ use failure::Fallible; use taskchampion::Uuid; +mod inmemory; + +pub(crate) use inmemory::InMemorySyncServer; + +/// The distinguished value for "no version" +pub const NO_VERSION_ID: VersionId = Uuid::nil(); + pub(crate) type HistorySegment = Vec; pub(crate) type ClientId = Uuid; pub(crate) type VersionId = Uuid; @@ -33,38 +40,3 @@ pub(crate) trait SyncServer: Sync + Send { history_segment: HistorySegment, ) -> Fallible; } - -// TODO: temporary -/// A "null" sync server's implementation; HTTP API methods call through to methods on a single -/// instance of this type. -pub(crate) struct NullSyncServer {} - -impl NullSyncServer { - pub(crate) fn new() -> Self { - Self {} - } -} - -impl SyncServer for NullSyncServer { - fn get_child_version( - &self, - _client_id: ClientId, - parent_version_id: VersionId, - ) -> Fallible> { - Ok(Some(GetVersionResult { - version_id: Uuid::new_v4(), - parent_version_id, - history_segment: b"abcd".to_vec(), - })) - } - - fn add_version( - &self, - _client_id: ClientId, - _parent_version_id: VersionId, - _history_segment: HistorySegment, - ) -> Fallible { - //Ok(AddVersionResult::Ok(Uuid::new_v4())) - Ok(AddVersionResult::ExpectedParentVersion(Uuid::new_v4())) - } -} From 3fb2327a5b08ae9835d5bc164c9fc85d5fdeabc4 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 26 Nov 2020 14:10:46 -0500 Subject: [PATCH 6/9] add docs for replica/server protocol --- docs/src/SUMMARY.md | 4 +- docs/src/sync-model.md | 128 +++++++++++++++++++++++++++++++++++++ docs/src/sync-protocol.md | 92 ++++++++++++++++++++++++++ docs/src/sync.md | 131 ++------------------------------------ 4 files changed, 228 insertions(+), 127 deletions(-) create mode 100644 docs/src/sync-model.md create mode 100644 docs/src/sync-protocol.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 4555c20c6..91a180ab5 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -7,5 +7,7 @@ - [Replica Storage](./storage.md) - [Task Database](./taskdb.md) - [Tasks](./tasks.md) -- [Synchronization](./sync.md) +- [Synchronization and the Sync Server](./sync.md) + - [Synchronization Model](./sync-model.md) + * [Server-Replica Protocol](./sync-protocol.md) - [Planned Functionality](./plans.md) diff --git a/docs/src/sync-model.md b/docs/src/sync-model.md new file mode 100644 index 000000000..691312efa --- /dev/null +++ b/docs/src/sync-model.md @@ -0,0 +1,128 @@ +# Synchronization Model + +The [task database](./taskdb.md) also implements synchronization. +Synchronization occurs between disconnected replicas, mediated by a server. +The replicas never communicate directly with one another. +The server does not have access to the task data; it sees only opaque blobs of data with a small amount of metadata. + +The synchronization process is a critical part of the task database's functionality, and it cannot function efficiently without occasional synchronization operations + +## Operational Transforms + +Synchronization is based on [operational transformation](https://en.wikipedia.org/wiki/Operational_transformation). +This section will assume some familiarity with the concept. + +## State and Operations + +At a given time, the set of tasks in a replica's storage is the essential "state" of that replica. +All modifications to that state occur via operations, as defined in [Replica Storage](./storage.md). +We can draw a network, or graph, with the nodes representing states and the edges representing operations. +For example: + +```text + o -- State: {abc-d123: 'get groceries', priority L} + | + | -- Operation: set abc-d123 priority to H + | + o -- State: {abc-d123: 'get groceries', priority H} +``` + +For those familiar with distributed version control systems, a state is analogous to a revision, while an operation is analogous to a commit. + +Fundamentally, synchronization involves all replicas agreeing on a single, linear sequence of operations and the state that those operations create. +Since the replicas are not connected, each may have additional operations that have been applied locally, but which have not yet been agreed on. +The synchronization process uses operational transformation to "linearize" those operations. +This process is analogous (vaguely) to rebasing a sequence of Git commits. + +### Versions + +Occasionally, database states are given a name (that takes the form of a UUID). +The system as a whole (all replicas) constructs a branch-free sequence of versions and the operations that separate each version from the next. +The version with the nil UUID is implicitly the empty database. + +The server stores the operations to change a state from a "parent" version to a "child" version, and provides that information as needed to replicas. +Replicas use this information to update their local task databases, and to generate new versions to send to the server. + +Replicas generate a new version to transmit local changes to the server. +The changes are represented as a sequence of operations with the state resulting from the final operation corresponding to the version. +In order to keep the versions in a single sequence, the server will only accept a proposed version from a replica if its parent version matches the latest version on the server. + +In the non-conflict case (such as with a single replica), then, a replica's synchronization process involves gathering up the operations it has accumulated since its last synchronization; bundling those operations into a version; and sending that version to the server. + +### Replica Invariant + +The replica's [storage](./storage.md) contains the current state in `tasks`, the as-yet un-synchronized operations in `operations`, and the last version at which synchronization occurred in `base_version`. + +The replica's un-synchronized operations are already reflected in its local `tasks`, so the following invariant holds: + +> Applying `operations` to the set of tasks at `base_version` gives a set of tasks identical +> to `tasks`. + +### Transformation + +When the latest version on the server contains operations that are not present in the replica, then the states have diverged. +For example: + +```text + o -- version N + w|\a + o o + x| \b + o o + y| \c + o o -- replica's local state + z| + o -- version N+1 +``` + +(diagram notation: `o` designates a state, lower-case letters designate operations, and versions are presented as if they were numbered sequentially) + +In this situation, the replica must "rebase" the local operations onto the latest version from the server and try again. +This process is performed using operational transformation (OT). +The result of this transformation is a sequence of operations based on the latest version, and a sequence of operations the replica can apply to its local task database to reach the same state +Continuing the example above, the resulting operations are shown with `'`: + +```text + o -- version N + w|\a + o o + x| \b + o o + y| \c + o o -- replica's intermediate local state + z| |w' + o-N+1 o + a'\ |x' + o o + b'\ |y' + o o + c'\|z' + o -- version N+2 +``` + +The replica applies w' through z' locally, and sends a' through c' to the server as the operations to generate version N+2. +Either path through this graph, a-b-c-w'-x'-y'-z' or a'-b'-c'-w-x-y-z, must generate *precisely* the same final state at version N+2. +Careful selection of the operations and the transformation function ensure this. + +See the comments in the source code for the details of how this transformation process is implemented. + +## Synchronization Process + +To perform a synchronization, the replica first requests the child version of `base_version` from the server (GetChildVersion). +It applies that version to its local `tasks`, rebases its local `operations` as described above, and updates `base_version`. +The replica repeats this process until the server indicates no additional child versions exist. +If there are no un-synchronized local operations, the process is complete. + +Otherwise, the replica creates a new version containing its local operations, giving its `base_version` as the parent version, and transmits that to the server (AddVersion). +In most cases, this will succeed, but if another replica has created a new version in the interim, then the new version will conflict with that other replica's new version and the server will respond with the new expected parent version. +In this case, the process repeats. +If the server indicates a conflict twice with the same expected base version, that is an indication that the replica has diverged (something serious has gone wrong). + +## Servers + +A replica depends on periodic synchronization for performant operation. +Without synchronization, its list of pending operations would grow indefinitely, and tasks could never be expired. +So all replicas, even "singleton" replicas which do not replicate task data with any other replica, must synchronize periodically. + +TaskChampion provides a `LocalServer` for this purpose. +It implements the `get_child_version` and `add_version` operations as described, storing data on-disk locally, all within the `task` binary. diff --git a/docs/src/sync-protocol.md b/docs/src/sync-protocol.md new file mode 100644 index 000000000..9a5caa247 --- /dev/null +++ b/docs/src/sync-protocol.md @@ -0,0 +1,92 @@ +# Server-Replica Protocol + +The server-replica protocol is defined abstractly in terms of request/response transactions from the replica to the server. +This is made concrete in an HTTP representation. + +The protocol builds on the model presented in the previous chapter, and in particular on the synchronization process. + +## Clients + +From the server's perspective, replicas are indistinguishable, so this protocol uses the term "client" to refer generically to all replicas replicating a single task history. + +## Server + +For each client, the server is responsible for storing the task history, in the form of a branch-free sequence of versions. + +For each client, it stores a set of versions as well as the latest version ID, defaulting to the nil UUID. +Each version has a version ID, a parent version ID, and a history segment (opaque data containing the operations for that version). +The server should maintain the following invariants: + +1. Given a client c, c.latestVersion is nil or exists in the set of versions. +1. Given versions v1 and v2 for a client, with v1.versionId != v2.versionId and v1.parentVersionId != nil, v1.parentVersionId != v2.parentVersionId. + In other words, versions do not branch. + +Note that versions form a linked list beginning with the version stored in he client. +This linked list need not continue back to a version with v.parentVersionId = nil. +It may end at any point when v.parentVersionId is not found in the set of Versions. +This observation allows the server to discard older versions. + +## Transactions + +### AddVersion + +The AddVersion transaction requests that the server add a new version to the client's task history. +The request contains the following; + + * parent version ID + * history segment + +The server determines whether the new version is acceptable, atomically with respect to other requests for the same client. +If it has no versions for the client, it accepts the version. +If it already has one or more versions for the client, then it accepts the version only if the given parent version ID matches its stored latest parent ID. + +If the version is accepted, the server generates a new version ID for it. +The version is added to the set of versions for the client, the client's latest version ID is set to the new version ID. +The new version ID is returned in the response to the client. + +If the version is not accepted, the server makes no changes, but responds to the client with a conflict indication containing the latest version ID. +The client may then "rebase" its operations and try again. +Note that if a client receives two conflict responses with the same parent version ID, it is an indication that the client's version history has diverged from that on the server. + +### GetChildVersion + +The GetChildVersion transaction is a read-only request for a version. +The request consists of a parent version ID. +The server searches its set of versions for a version with the given parent ID. +If found, it returns the version's + + * version ID, + * parent version ID (matching that in the request), and + * history segment. + +If not found, the server returns a negative response. + +## HTTP Representation + +The transactions above are realized for an HTTP server at `` using the HTTP requests and responses described here. +The `origin` *should* be an HTTPS endpoint on general principle, but nothing in the functonality or security of the protocol depends on connection encryption. + +The replica identifies itself to the server using a `clientId` in the form of a UUID. + +### AddVersion + +The request is a `POST` to `/client//add-version/`. +The request body contains the history segment, optionally encoded using any encoding supported by actix-web. +The content-type must be `application/vnd.taskchampion.history-segment`. + +The success response is a 200 OK with an empty body. +The new version ID appears in the `X-Version-Id` header. + +On conflict, the response is a 409 CONFLICT with an empty body. +The expected parent version ID appears in the `X-Parent-Version-Id` header. + +Other error responses (4xx or 5xx) may be returned and should be treated appropriately to their meanings in the HTTP specification. + +### GetChildVersion + +The request is a `GET` to `/client//get-child-version/`. +The response is 404 NOT FOUND if no such version exists. +Otherwise, the response is a 200 OK. +The version's history segment is returned in the response body, with content-type `application/vnd.taskchampion.history-segment`. +The version ID appears in the `X-Version-Id` header. +The response body may be encoded, in accordance with any `Accept-Encoding` header in the request. diff --git a/docs/src/sync.md b/docs/src/sync.md index cd2621cdc..fed75d17f 100644 --- a/docs/src/sync.md +++ b/docs/src/sync.md @@ -1,128 +1,7 @@ -# Synchronization +# Synchronization and the Sync Server -The [task database](./taskdb.md) also implements synchronization. -Synchronization occurs between disconnected replicas, mediated by a server. -The replicas never communicate directly with one another. -The server does not have access to the task data; it sees only opaque blobs of data with a small amount of metadata. +This section covers *synchronization* of *replicas* containing the same set of tasks. +A replica is can perform all operations locally without connecting to a sync server, then share those operations with other replicas when it connects. +Sync is a critical feature of TaskChampion, allowing users to consult and update the same task list on multiple devices, without requiring constant connection. -The synchronization process is a critical part of the task database's functionality, and it cannot function efficiently without occasional synchronization operations - -## Operational Transformations - -Synchronization is based on [operational transformation](https://en.wikipedia.org/wiki/Operational_transformation). -This section will assume some familiarity with the concept. - -## State and Operations - -At a given time, the set of tasks in a replica's storage is the essential "state" of that replica. -All modifications to that state occur via operations, as defined in [Replica Storage](./storage.md). -We can draw a network, or graph, with the nodes representing states and the edges representing operations. -For example: - -```text - o -- State: {abc-d123: 'get groceries', priority L} - | - | -- Operation: set abc-d123 priority to H - | - o -- State: {abc-d123: 'get groceries', priority H} -``` - -For those familiar with distributed version control systems, a state is analogous to a revision, while an operation is analogous to a commit. - -Fundamentally, synchronization involves all replicas agreeing on a single, linear sequence of operations and the state that those operations create. -Since the replicas are not connected, each may have additional operations that have been applied locally, but which have not yet been agreed on. -The synchronization process uses operational transformation to "linearize" those operations. -This process is analogous (vaguely) to rebasing a sequence of Git commits. - -### Versions - -Occasionally, database states are given a name (that takes the form of a UUID). -The system as a whole (all replicas) constructs a branch-free sequence of versions and the operations that separate each version from the next. -The version with the nil UUID is implicitly the empty database. - -The server stores the operations to change a state from a "parent" version to a "child" version, and provides that information as needed to replicas. -Replicas use this information to update their local task databases, and to generate new versions to send to the server. - -Replicas generate a new version to transmit local changes to the server. -The changes are represented as a sequence of operations with the state resulting from the final operation corresponding to the version. -In order to keep the versions in a single sequence, the server will only accept a proposed version from a replica if its parent version matches the latest version on the server. - -In the non-conflict case (such as with a single replica), then, a replica's synchronization process involves gathering up the operations it has accumulated since its last synchronization; bundling those operations into a version; and sending that version to the server. - -### Replica Invariant - -The replica's [storage](./storage.md) contains the current state in `tasks`, the as-yet un-synchronized operations in `operations`, and the last version at which synchronization occurred in `base_version`. - -The replica's un-synchronized operations are already reflected in its local `tasks`, so the following invariant holds: - -> Applying `operations` to the set of tasks at `base_version` gives a set of tasks identical -> to `tasks`. - -### Transformation - -When the latest version on the server contains operations that are not present in the replica, then the states have diverged. -For example: - -```text - o -- version N - w|\a - o o - x| \b - o o - y| \c - o o -- replica's local state - z| - o -- version N+1 -``` - -(diagram notation: `o` designates a state, lower-case letters designate operations, and versions are presented as if they were numbered sequentially) - -In this situation, the replica must "rebase" the local operations onto the latest version from the server and try again. -This process is performed using operational transformation (OT). -The result of this transformation is a sequence of operations based on the latest version, and a sequence of operations the replica can apply to its local task database to reach the same state -Continuing the example above, the resulting operations are shown with `'`: - -```text - o -- version N - w|\a - o o - x| \b - o o - y| \c - o o -- replica's intermediate local state - z| |w' - o-N+1 o - a'\ |x' - o o - b'\ |y' - o o - c'\|z' - o -- version N+2 -``` - -The replica applies w' through z' locally, and sends a' through c' to the server as the operations to generate version N+2. -Either path through this graph, a-b-c-w'-x'-y'-z' or a'-b'-c'-w-x-y-z, must generate *precisely* the same final state at version N+2. -Careful selection of the operations and the transformation function ensure this. - -See the comments in the source code for the details of how this transformation process is implemented. - -## Synchronization Process - -To perform a synchronization, the replica first requests the child version of `base_version` from the server (`get_child_version`). -It applies that version to its local `tasks`, rebases its local `operations` as described above, and updates `base_version`. -The replica repeats this process until the server indicates no additional child versions exist. -If there are no un-synchronized local operations, the process is complete. - -Otherwise, the replica creates a new version containing its local operations, giving its `base_version` as the parent version, and transmits that to the server (`add_version`). -In most cases, this will succeed, but if another replica has created a new version in the interim, then the new version will conflict with that other replica's new version and the server will respond with the new expected parent version. -In this case, the process repeats. -If the server indicates a conflict twice with the same expected base version, that is an indication that the replica has diverged (something serious has gone wrong). - -## Servers - -A replica depends on periodic synchronization for performant operation. -Without synchronization, its list of pending operations would grow indefinitely, and tasks could never be expired. -So all replicas, even "singleton" replicas which do not replicate task data with any other replica, must synchronize periodically. - -TaskChampion provides a `LocalServer` for this purpose. -It implements the `get_child_version` and `add_version` operations as described, storing data on-disk locally, all within the `task` binary. +This is a complex topic, and the section is broken into several chapters, beginning at the lower levels of the implementation and working up. From 7472749fee861b3cc4db463a29069a6cdc47dbcf Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 26 Nov 2020 17:27:17 -0500 Subject: [PATCH 7/9] add tests for API methods --- Cargo.lock | 1 + sync-server/Cargo.toml | 5 +- sync-server/src/api/add_version.rs | 123 +++++++++++++++++++++++ sync-server/src/api/get_child_version.rs | 78 ++++++++++++++ sync-server/src/api/mod.rs | 11 +- sync-server/src/main.rs | 35 +++++-- sync-server/src/server/mod.rs | 3 + sync-server/src/test.rs | 57 +++++++++++ 8 files changed, 299 insertions(+), 14 deletions(-) create mode 100644 sync-server/src/test.rs diff --git a/Cargo.lock b/Cargo.lock index 4c744d952..103d2e8ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2035,6 +2035,7 @@ dependencies = [ name = "sync-server" version = "0.1.0" dependencies = [ + "actix-rt", "actix-web", "failure", "futures", diff --git a/sync-server/Cargo.toml b/sync-server/Cargo.toml index 3c638aa7c..e9df49ac7 100644 --- a/sync-server/Cargo.toml +++ b/sync-server/Cargo.toml @@ -9,5 +9,8 @@ edition = "2018" [dependencies] actix-web = "3.3.0" failure = "0.1.8" -futures = "0.3.8" taskchampion = { path = "../taskchampion" } +futures = "0.3.8" + +[dev-dependencies] +actix-rt = "1.1.1" diff --git a/sync-server/src/api/add_version.rs b/sync-server/src/api/add_version.rs index 83f010ef5..4dc8393cf 100644 --- a/sync-server/src/api/add_version.rs +++ b/sync-server/src/api/add_version.rs @@ -43,6 +43,10 @@ pub(crate) async fn service( body.extend_from_slice(&chunk); } + if body.is_empty() { + return Err(error::ErrorBadRequest("Empty body")); + } + let result = data .add_version(client_id, parent_version_id, body.to_vec()) .map_err(|e| error::InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?; @@ -55,3 +59,122 @@ pub(crate) async fn service( .body(""), }) } + +#[cfg(test)] +mod test { + use super::*; + use crate::api::ServerState; + use crate::app_scope; + use crate::server::SyncServer; + use crate::test::TestServer; + use actix_web::{test, App}; + use taskchampion::Uuid; + + #[actix_rt::test] + async fn test_success() { + let client_id = Uuid::new_v4(); + let version_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + expected_client_id: client_id, + av_expected_parent_version_id: parent_version_id, + av_expected_history_segment: b"abcd".to_vec(), + av_result: Some(AddVersionResult::Ok(version_id)), + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!("/client/{}/add-version/{}", client_id, parent_version_id); + let req = test::TestRequest::post() + .uri(&uri) + .header( + "Content-Type", + "application/vnd.taskchampion.history-segment", + ) + .set_payload(b"abcd".to_vec()) + .to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get("X-Version-Id").unwrap(), + &version_id.to_string() + ); + assert_eq!(resp.headers().get("X-Parent-Version-Id"), None); + } + + #[actix_rt::test] + async fn test_conflict() { + let client_id = Uuid::new_v4(); + let version_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + expected_client_id: client_id, + av_expected_parent_version_id: parent_version_id, + av_expected_history_segment: b"abcd".to_vec(), + av_result: Some(AddVersionResult::ExpectedParentVersion(version_id)), + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!("/client/{}/add-version/{}", client_id, parent_version_id); + let req = test::TestRequest::post() + .uri(&uri) + .header( + "Content-Type", + "application/vnd.taskchampion.history-segment", + ) + .set_payload(b"abcd".to_vec()) + .to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::CONFLICT); + assert_eq!(resp.headers().get("X-Version-Id"), None); + assert_eq!( + resp.headers().get("X-Parent-Version-Id").unwrap(), + &version_id.to_string() + ); + } + + #[actix_rt::test] + async fn test_bad_content_type() { + let client_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!("/client/{}/add-version/{}", client_id, parent_version_id); + let req = test::TestRequest::post() + .uri(&uri) + .header("Content-Type", "not/correct") + .set_payload(b"abcd".to_vec()) + .to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[actix_rt::test] + async fn test_empty_body() { + let client_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!("/client/{}/add-version/{}", client_id, parent_version_id); + let req = test::TestRequest::post() + .uri(&uri) + .header( + "Content-Type", + "application/vnd.taskchampion.history-segment", + ) + .to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } +} diff --git a/sync-server/src/api/get_child_version.rs b/sync-server/src/api/get_child_version.rs index 4056408ce..b16f5219b 100644 --- a/sync-server/src/api/get_child_version.rs +++ b/sync-server/src/api/get_child_version.rs @@ -33,3 +33,81 @@ pub(crate) async fn service( Err(error::ErrorNotFound("no such version")) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::api::ServerState; + use crate::app_scope; + use crate::server::{GetVersionResult, SyncServer}; + use crate::test::TestServer; + use actix_web::{test, App}; + use taskchampion::Uuid; + + #[actix_rt::test] + async fn test_success() { + let client_id = Uuid::new_v4(); + let version_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + expected_client_id: client_id, + gcv_expected_parent_version_id: parent_version_id, + gcv_result: Some(GetVersionResult { + version_id: version_id, + parent_version_id: parent_version_id, + history_segment: b"abcd".to_vec(), + }), + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!( + "/client/{}/get-child-version/{}", + client_id, parent_version_id + ); + let req = test::TestRequest::get().uri(&uri).to_request(); + let mut resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get("X-Version-Id").unwrap(), + &version_id.to_string() + ); + assert_eq!( + resp.headers().get("X-Parent-Version-Id").unwrap(), + &parent_version_id.to_string() + ); + assert_eq!( + resp.headers().get("Content-Type").unwrap(), + &"application/vnd.taskchampion.history-segment".to_string() + ); + + use futures::StreamExt; + let (bytes, _) = resp.take_body().into_future().await; + assert_eq!(bytes.unwrap().unwrap().as_ref(), b"abcd"); + } + + #[actix_rt::test] + async fn test_not_found() { + let client_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + expected_client_id: client_id, + gcv_expected_parent_version_id: parent_version_id, + gcv_result: None, + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!( + "/client/{}/get-child-version/{}", + client_id, parent_version_id + ); + let req = test::TestRequest::get().uri(&uri).to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + assert_eq!(resp.headers().get("X-Version-Id"), None); + assert_eq!(resp.headers().get("X-Parent-Version-Id"), None); + } +} diff --git a/sync-server/src/api/mod.rs b/sync-server/src/api/mod.rs index 9dcca18bc..c58d2420b 100644 --- a/sync-server/src/api/mod.rs +++ b/sync-server/src/api/mod.rs @@ -1,8 +1,9 @@ use crate::server::SyncServer; +use actix_web::{web, Scope}; use std::sync::Arc; -pub(crate) mod add_version; -pub(crate) mod get_child_version; +mod add_version; +mod get_child_version; /// The content-type for history segments (opaque blobs of bytes) pub(crate) const HISTORY_SEGMENT_CONTENT_TYPE: &str = @@ -16,3 +17,9 @@ pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id"; /// The type containing a reference to the SyncServer object in the Actix state. pub(crate) type ServerState = Arc>; + +pub(crate) fn api_scope() -> Scope { + web::scope("") + .service(get_child_version::service) + .service(add_version::service) +} diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs index a8e4cc9de..3123c80db 100644 --- a/sync-server/src/main.rs +++ b/sync-server/src/main.rs @@ -1,24 +1,37 @@ -use actix_web::{App, HttpServer}; -use api::ServerState; +use actix_web::{get, web, App, HttpServer, Responder, Scope}; +use api::{api_scope, ServerState}; use server::{InMemorySyncServer, SyncServer}; mod api; mod server; +#[cfg(test)] +mod test; + // TODO: use hawk to sign requests +#[get("/")] +async fn index() -> impl Responder { + // TODO: add version here + "TaskChampion sync server" +} + +/// Return a scope defining the URL rules for this server, with access to +/// the given ServerState. +pub(crate) fn app_scope(server_state: ServerState) -> Scope { + web::scope("") + .data(server_state) + .service(index) + .service(api_scope()) +} + #[actix_web::main] async fn main() -> std::io::Result<()> { let server_box: Box = Box::new(InMemorySyncServer::new()); let server_state = ServerState::new(server_box); - HttpServer::new(move || { - App::new() - .data(server_state.clone()) - .service(api::get_child_version::service) - .service(api::add_version::service) - }) - .bind("127.0.0.1:8080")? - .run() - .await + HttpServer::new(move || App::new().service(app_scope(server_state.clone()))) + .bind("127.0.0.1:8080")? + .run() + .await } diff --git a/sync-server/src/server/mod.rs b/sync-server/src/server/mod.rs index 6d8593c03..7768c0c08 100644 --- a/sync-server/src/server/mod.rs +++ b/sync-server/src/server/mod.rs @@ -13,6 +13,7 @@ pub(crate) type ClientId = Uuid; pub(crate) type VersionId = Uuid; /// Response to get_child_version +#[derive(Clone)] pub(crate) struct GetVersionResult { pub(crate) version_id: Uuid, pub(crate) parent_version_id: Uuid, @@ -20,12 +21,14 @@ pub(crate) struct GetVersionResult { } /// Response to add_version +#[derive(Clone)] pub(crate) enum AddVersionResult { /// OK, version added with the given ID Ok(VersionId), /// Rejected; expected a version with the given parent version ExpectedParentVersion(VersionId), } + pub(crate) trait SyncServer: Sync + Send { fn get_child_version( &self, diff --git a/sync-server/src/test.rs b/sync-server/src/test.rs new file mode 100644 index 000000000..b9f68a66d --- /dev/null +++ b/sync-server/src/test.rs @@ -0,0 +1,57 @@ +use crate::api::ServerState; +use crate::app_scope; +use crate::server::{ + AddVersionResult, ClientId, GetVersionResult, HistorySegment, SyncServer, VersionId, +}; +use actix_web::{test, App}; +use failure::Fallible; + +#[derive(Default)] +pub(crate) struct TestServer { + /// test server will panic if this is not given + pub expected_client_id: ClientId, + + pub gcv_expected_parent_version_id: VersionId, + pub gcv_result: Option, + + pub av_expected_parent_version_id: VersionId, + pub av_expected_history_segment: HistorySegment, + pub av_result: Option, +} + +impl SyncServer for TestServer { + fn get_child_version( + &self, + client_id: ClientId, + parent_version_id: VersionId, + ) -> Fallible> { + assert_eq!(client_id, self.expected_client_id); + assert_eq!(parent_version_id, self.gcv_expected_parent_version_id); + Ok(self.gcv_result.clone()) + } + + fn add_version( + &self, + client_id: ClientId, + parent_version_id: VersionId, + history_segment: HistorySegment, + ) -> Fallible { + assert_eq!(client_id, self.expected_client_id); + assert_eq!(parent_version_id, self.av_expected_parent_version_id); + assert_eq!(history_segment, self.av_expected_history_segment); + Ok(self.av_result.clone().unwrap()) + } +} + +#[actix_rt::test] +async fn test_index_get() { + let server_box: Box = Box::new(TestServer { + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let req = test::TestRequest::get().uri("/").to_request(); + let resp = test::call_service(&mut app, req).await; + assert!(resp.status().is_success()); +} From fb22b9686ff32477220d6e6b7d190d8233af6d21 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 26 Nov 2020 19:19:51 -0500 Subject: [PATCH 8/9] refactor sync server to use pluggable storage ..with a fixed implementation of the replica / server protocol logic. There isn't much logic yet, and there's a lot of boilerplate to take care of, so this looks a little lopsided, but I'm confident this is the right structure for this code's future. --- sync-server/src/api/add_version.rs | 110 +++++++++++++++-------- sync-server/src/api/get_child_version.rs | 97 ++++++++++++++------ sync-server/src/api/mod.rs | 13 ++- sync-server/src/main.rs | 27 ++++-- sync-server/src/server/inmemory.rs | 108 ---------------------- sync-server/src/server/mod.rs | 20 ----- sync-server/src/storage/inmemory.rs | 90 +++++++++++++++++++ sync-server/src/storage/mod.rs | 56 ++++++++++++ sync-server/src/test.rs | 57 ------------ 9 files changed, 319 insertions(+), 259 deletions(-) delete mode 100644 sync-server/src/server/inmemory.rs create mode 100644 sync-server/src/storage/inmemory.rs create mode 100644 sync-server/src/storage/mod.rs delete mode 100644 sync-server/src/test.rs diff --git a/sync-server/src/api/add_version.rs b/sync-server/src/api/add_version.rs index 4dc8393cf..2413c5fce 100644 --- a/sync-server/src/api/add_version.rs +++ b/sync-server/src/api/add_version.rs @@ -1,11 +1,13 @@ use crate::api::{ - ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER, -}; -use crate::server::{AddVersionResult, ClientId, VersionId}; -use actix_web::{ - error, http::StatusCode, post, web, HttpMessage, HttpRequest, HttpResponse, Result, + failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, + VERSION_ID_HEADER, }; +use crate::server::{AddVersionResult, ClientId, HistorySegment, VersionId, NO_VERSION_ID}; +use crate::storage::{Client, StorageTxn}; +use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result}; +use failure::Fallible; use futures::StreamExt; +use taskchampion::Uuid; /// Max history segment size: 100MB const MAX_SIZE: usize = 100 * 1024 * 1024; @@ -23,7 +25,7 @@ const MAX_SIZE: usize = 100 * 1024 * 1024; #[post("/client/{client_id}/add-version/{parent_version_id}")] pub(crate) async fn service( req: HttpRequest, - data: web::Data, + server_state: web::Data, web::Path((client_id, parent_version_id)): web::Path<(ClientId, VersionId)>, mut payload: web::Payload, ) -> Result { @@ -47,9 +49,18 @@ pub(crate) async fn service( return Err(error::ErrorBadRequest("Empty body")); } - let result = data - .add_version(client_id, parent_version_id, body.to_vec()) - .map_err(|e| error::InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?; + // note that we do not open the transaction until the body has been read + // completely, to avoid blocking other storage access while that data is + // in transit. + let mut txn = server_state.txn().map_err(failure_to_ise)?; + + let client = txn + .get_client(client_id) + .map_err(failure_to_ise)? + .ok_or_else(|| error::ErrorNotFound("no such client"))?; + + let result = add_version(txn, client_id, client, parent_version_id, body.to_vec()) + .map_err(failure_to_ise)?; Ok(match result { AddVersionResult::Ok(version_id) => HttpResponse::Ok() .header(VERSION_ID_HEADER, version_id.to_string()) @@ -60,14 +71,37 @@ pub(crate) async fn service( }) } +fn add_version<'a>( + mut txn: Box, + client_id: ClientId, + client: Client, + parent_version_id: VersionId, + history_segment: HistorySegment, +) -> Fallible { + // check if this version is acceptable, under the protection of the transaction + if client.latest_version_id != NO_VERSION_ID && parent_version_id != client.latest_version_id { + return Ok(AddVersionResult::ExpectedParentVersion( + client.latest_version_id, + )); + } + + // invent a version ID + let version_id = Uuid::new_v4(); + + // update the DB + txn.add_version(client_id, version_id, parent_version_id, history_segment)?; + txn.set_client_latest_version_id(client_id, version_id)?; + txn.commit()?; + + Ok(AddVersionResult::Ok(version_id)) +} + #[cfg(test)] mod test { - use super::*; use crate::api::ServerState; use crate::app_scope; - use crate::server::SyncServer; - use crate::test::TestServer; - use actix_web::{test, App}; + use crate::storage::{InMemoryStorage, Storage}; + use actix_web::{http::StatusCode, test, App}; use taskchampion::Uuid; #[actix_rt::test] @@ -75,13 +109,15 @@ mod test { let client_id = Uuid::new_v4(); let version_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4(); - let server_box: Box = Box::new(TestServer { - expected_client_id: client_id, - av_expected_parent_version_id: parent_version_id, - av_expected_history_segment: b"abcd".to_vec(), - av_result: Some(AddVersionResult::Ok(version_id)), - ..Default::default() - }); + let server_box: Box = Box::new(InMemoryStorage::new()); + + // set up the storage contents.. + { + let mut txn = server_box.txn().unwrap(); + txn.set_client_latest_version_id(client_id, Uuid::nil()) + .unwrap(); + } + let server_state = ServerState::new(server_box); let mut app = test::init_service(App::new().service(app_scope(server_state))).await; @@ -96,10 +132,12 @@ mod test { .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get("X-Version-Id").unwrap(), - &version_id.to_string() - ); + + // the returned version ID is random, but let's check that it's not + // the passed parent version ID, at least + let new_version_id = resp.headers().get("X-Version-Id").unwrap(); + assert!(new_version_id != &version_id.to_string()); + assert_eq!(resp.headers().get("X-Parent-Version-Id"), None); } @@ -108,13 +146,15 @@ mod test { let client_id = Uuid::new_v4(); let version_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4(); - let server_box: Box = Box::new(TestServer { - expected_client_id: client_id, - av_expected_parent_version_id: parent_version_id, - av_expected_history_segment: b"abcd".to_vec(), - av_result: Some(AddVersionResult::ExpectedParentVersion(version_id)), - ..Default::default() - }); + let server_box: Box = Box::new(InMemoryStorage::new()); + + // set up the storage contents.. + { + let mut txn = server_box.txn().unwrap(); + txn.set_client_latest_version_id(client_id, version_id) + .unwrap(); + } + let server_state = ServerState::new(server_box); let mut app = test::init_service(App::new().service(app_scope(server_state))).await; @@ -140,9 +180,7 @@ mod test { async fn test_bad_content_type() { let client_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4(); - let server_box: Box = Box::new(TestServer { - ..Default::default() - }); + let server_box: Box = Box::new(InMemoryStorage::new()); let server_state = ServerState::new(server_box); let mut app = test::init_service(App::new().service(app_scope(server_state))).await; @@ -160,9 +198,7 @@ mod test { async fn test_empty_body() { let client_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4(); - let server_box: Box = Box::new(TestServer { - ..Default::default() - }); + let server_box: Box = Box::new(InMemoryStorage::new()); let server_state = ServerState::new(server_box); let mut app = test::init_service(App::new().service(app_scope(server_state))).await; diff --git a/sync-server/src/api/get_child_version.rs b/sync-server/src/api/get_child_version.rs index b16f5219b..84af0b179 100644 --- a/sync-server/src/api/get_child_version.rs +++ b/sync-server/src/api/get_child_version.rs @@ -1,8 +1,11 @@ use crate::api::{ - ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER, + failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, + VERSION_ID_HEADER, }; -use crate::server::{ClientId, VersionId}; -use actix_web::{error, get, http::StatusCode, web, HttpResponse, Result}; +use crate::server::{ClientId, GetVersionResult, VersionId}; +use crate::storage::StorageTxn; +use actix_web::{error, get, web, HttpResponse, Result}; +use failure::Fallible; /// Get a child version. /// @@ -14,12 +17,16 @@ use actix_web::{error, get, http::StatusCode, web, HttpResponse, Result}; /// Returns other 4xx or 5xx responses on other errors. #[get("/client/{client_id}/get-child-version/{parent_version_id}")] pub(crate) async fn service( - data: web::Data, + server_state: web::Data, web::Path((client_id, parent_version_id)): web::Path<(ClientId, VersionId)>, ) -> Result { - let result = data - .get_child_version(client_id, parent_version_id) - .map_err(|e| error::InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?; + let mut txn = server_state.txn().map_err(failure_to_ise)?; + + txn.get_client(client_id) + .map_err(failure_to_ise)? + .ok_or_else(|| error::ErrorNotFound("no such client"))?; + + let result = get_child_version(txn, client_id, parent_version_id).map_err(failure_to_ise)?; if let Some(result) = result { Ok(HttpResponse::Ok() .content_type(HISTORY_SEGMENT_CONTENT_TYPE) @@ -34,14 +41,26 @@ pub(crate) async fn service( } } +fn get_child_version<'a>( + mut txn: Box, + client_id: ClientId, + parent_version_id: VersionId, +) -> Fallible> { + Ok(txn + .get_version_by_parent(client_id, parent_version_id)? + .map(|version| GetVersionResult { + version_id: version.version_id, + parent_version_id: version.parent_version_id, + history_segment: version.history_segment, + })) +} + #[cfg(test)] mod test { - use super::*; use crate::api::ServerState; use crate::app_scope; - use crate::server::{GetVersionResult, SyncServer}; - use crate::test::TestServer; - use actix_web::{test, App}; + use crate::storage::{InMemoryStorage, Storage}; + use actix_web::{http::StatusCode, test, App}; use taskchampion::Uuid; #[actix_rt::test] @@ -49,16 +68,17 @@ mod test { let client_id = Uuid::new_v4(); let version_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4(); - let server_box: Box = Box::new(TestServer { - expected_client_id: client_id, - gcv_expected_parent_version_id: parent_version_id, - gcv_result: Some(GetVersionResult { - version_id: version_id, - parent_version_id: parent_version_id, - history_segment: b"abcd".to_vec(), - }), - ..Default::default() - }); + let server_box: Box = Box::new(InMemoryStorage::new()); + + // set up the storage contents.. + { + let mut txn = server_box.txn().unwrap(); + txn.set_client_latest_version_id(client_id, Uuid::new_v4()) + .unwrap(); + txn.add_version(client_id, version_id, parent_version_id, b"abcd".to_vec()) + .unwrap(); + } + let server_state = ServerState::new(server_box); let mut app = test::init_service(App::new().service(app_scope(server_state))).await; @@ -88,15 +108,36 @@ mod test { } #[actix_rt::test] - async fn test_not_found() { + async fn test_client_not_found() { let client_id = Uuid::new_v4(); let parent_version_id = Uuid::new_v4(); - let server_box: Box = Box::new(TestServer { - expected_client_id: client_id, - gcv_expected_parent_version_id: parent_version_id, - gcv_result: None, - ..Default::default() - }); + let server_box: Box = Box::new(InMemoryStorage::new()); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!( + "/client/{}/get-child-version/{}", + client_id, parent_version_id + ); + let req = test::TestRequest::get().uri(&uri).to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + assert_eq!(resp.headers().get("X-Version-Id"), None); + assert_eq!(resp.headers().get("X-Parent-Version-Id"), None); + } + + #[actix_rt::test] + async fn test_version_not_found() { + let client_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(InMemoryStorage::new()); + + // create the client, but not the version + { + let mut txn = server_box.txn().unwrap(); + txn.set_client_latest_version_id(client_id, Uuid::new_v4()) + .unwrap(); + } let server_state = ServerState::new(server_box); let mut app = test::init_service(App::new().service(app_scope(server_state))).await; diff --git a/sync-server/src/api/mod.rs b/sync-server/src/api/mod.rs index c58d2420b..cddcab59c 100644 --- a/sync-server/src/api/mod.rs +++ b/sync-server/src/api/mod.rs @@ -1,5 +1,5 @@ -use crate::server::SyncServer; -use actix_web::{web, Scope}; +use crate::storage::Storage; +use actix_web::{error, http::StatusCode, web, Scope}; use std::sync::Arc; mod add_version; @@ -15,11 +15,16 @@ pub(crate) const VERSION_ID_HEADER: &str = "X-Version-Id"; /// The header names for parent version ID pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id"; -/// The type containing a reference to the SyncServer object in the Actix state. -pub(crate) type ServerState = Arc>; +/// The type containing a reference to the Storage object in the Actix state. +pub(crate) type ServerState = Arc>; pub(crate) fn api_scope() -> Scope { web::scope("") .service(get_child_version::service) .service(add_version::service) } + +/// Convert a failure::Error to an Actix ISE +fn failure_to_ise(err: failure::Error) -> impl actix_web::ResponseError { + error::InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR) +} diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs index 3123c80db..7b91c5e3c 100644 --- a/sync-server/src/main.rs +++ b/sync-server/src/main.rs @@ -1,12 +1,10 @@ +use crate::storage::{InMemoryStorage, Storage}; use actix_web::{get, web, App, HttpServer, Responder, Scope}; use api::{api_scope, ServerState}; -use server::{InMemorySyncServer, SyncServer}; mod api; mod server; - -#[cfg(test)] -mod test; +mod storage; // TODO: use hawk to sign requests @@ -27,7 +25,7 @@ pub(crate) fn app_scope(server_state: ServerState) -> Scope { #[actix_web::main] async fn main() -> std::io::Result<()> { - let server_box: Box = Box::new(InMemorySyncServer::new()); + let server_box: Box = Box::new(InMemoryStorage::new()); let server_state = ServerState::new(server_box); HttpServer::new(move || App::new().service(app_scope(server_state.clone()))) @@ -35,3 +33,22 @@ async fn main() -> std::io::Result<()> { .run() .await } + +#[cfg(test)] +mod test { + use super::*; + use crate::api::ServerState; + use crate::storage::{InMemoryStorage, Storage}; + use actix_web::{test, App}; + + #[actix_rt::test] + async fn test_index_get() { + let server_box: Box = Box::new(InMemoryStorage::new()); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let req = test::TestRequest::get().uri("/").to_request(); + let resp = test::call_service(&mut app, req).await; + assert!(resp.status().is_success()); + } +} diff --git a/sync-server/src/server/inmemory.rs b/sync-server/src/server/inmemory.rs deleted file mode 100644 index 37302282e..000000000 --- a/sync-server/src/server/inmemory.rs +++ /dev/null @@ -1,108 +0,0 @@ -use super::{ - AddVersionResult, ClientId, GetVersionResult, HistorySegment, SyncServer, VersionId, - NO_VERSION_ID, -}; -use failure::Fallible; -use std::collections::HashMap; -use std::sync::{Mutex, RwLock}; -use taskchampion::Uuid; - -/// An in-memory server backend that can be useful for testing. -pub(crate) struct InMemorySyncServer { - clients: RwLock>>, -} - -struct Version { - version_id: VersionId, - history_segment: HistorySegment, -} - -struct Client { - latest_version_id: VersionId, - // NOTE: indexed by parent_version_id! - versions: HashMap, -} - -impl InMemorySyncServer { - pub(crate) fn new() -> Self { - Self { - clients: RwLock::new(HashMap::new()), - } - } -} - -impl SyncServer for InMemorySyncServer { - fn get_child_version( - &self, - client_id: ClientId, - parent_version_id: VersionId, - ) -> Fallible> { - let clients = self.clients.read().expect("poisoned lock"); - if let Some(client) = clients.get(&client_id) { - let client = client.lock().expect("poisoned lock"); - if let Some(version) = client.versions.get(&parent_version_id) { - return Ok(Some(GetVersionResult { - version_id: version.version_id, - parent_version_id, - history_segment: version.history_segment.clone(), - })); - } - } - Ok(None) - } - - fn add_version( - &self, - client_id: ClientId, - parent_version_id: VersionId, - history_segment: HistorySegment, - ) -> Fallible { - let mut clients = self.clients.write().expect("poisoned lock"); - if let Some(client) = clients.get_mut(&client_id) { - let mut client = client.lock().expect("poisoned lock"); - if client.latest_version_id != NO_VERSION_ID { - if parent_version_id != client.latest_version_id { - return Ok(AddVersionResult::ExpectedParentVersion( - client.latest_version_id, - )); - } - } - - // invent a new ID for this version - let version_id = Uuid::new_v4(); - - client.versions.insert( - parent_version_id, - Version { - version_id, - history_segment, - }, - ); - client.latest_version_id = version_id; - - Ok(AddVersionResult::Ok(version_id)) - } else { - // new client, so insert a client with just this new version - - let latest_version_id = Uuid::new_v4(); - let mut versions = HashMap::new(); - versions.insert( - parent_version_id, - Version { - version_id: latest_version_id, - history_segment, - }, - ); - - clients.insert( - client_id, - Mutex::new(Client { - latest_version_id, - versions, - }), - ); - - Ok(AddVersionResult::Ok(latest_version_id)) - } - } -} diff --git a/sync-server/src/server/mod.rs b/sync-server/src/server/mod.rs index 7768c0c08..9e2412ba3 100644 --- a/sync-server/src/server/mod.rs +++ b/sync-server/src/server/mod.rs @@ -1,10 +1,5 @@ -use failure::Fallible; use taskchampion::Uuid; -mod inmemory; - -pub(crate) use inmemory::InMemorySyncServer; - /// The distinguished value for "no version" pub const NO_VERSION_ID: VersionId = Uuid::nil(); @@ -28,18 +23,3 @@ pub(crate) enum AddVersionResult { /// Rejected; expected a version with the given parent version ExpectedParentVersion(VersionId), } - -pub(crate) trait SyncServer: Sync + Send { - fn get_child_version( - &self, - client_id: ClientId, - parent_version_id: VersionId, - ) -> Fallible>; - - fn add_version( - &self, - client_id: ClientId, - parent_version_id: VersionId, - history_segment: HistorySegment, - ) -> Fallible; -} diff --git a/sync-server/src/storage/inmemory.rs b/sync-server/src/storage/inmemory.rs new file mode 100644 index 000000000..91c868d42 --- /dev/null +++ b/sync-server/src/storage/inmemory.rs @@ -0,0 +1,90 @@ +use super::{Client, Storage, StorageTxn, Uuid, Version}; +use failure::Fallible; +use std::collections::HashMap; +use std::sync::{Mutex, MutexGuard}; + +struct Inner { + /// Clients, indexed by client_id + clients: HashMap, + + /// Versions, indexed by (client_id, parent_version_id) + versions: HashMap<(Uuid, Uuid), Version>, +} + +pub(crate) struct InMemoryStorage(Mutex); + +impl InMemoryStorage { + pub(crate) fn new() -> Self { + Self(Mutex::new(Inner { + clients: HashMap::new(), + versions: HashMap::new(), + })) + } +} + +struct InnerTxn<'a>(MutexGuard<'a, Inner>); + +/// In-memory storage for testing and experimentation. +/// +/// NOTE: this does not implement transaction rollback. +impl Storage for InMemoryStorage { + fn txn<'a>(&'a self) -> Fallible> { + Ok(Box::new(InnerTxn(self.0.lock().expect("poisoned lock")))) + } +} + +impl<'a> StorageTxn for InnerTxn<'a> { + fn get_client(&mut self, client_id: Uuid) -> Fallible> { + Ok(self.0.clients.get(&client_id).cloned()) + } + + fn set_client_latest_version_id( + &mut self, + client_id: Uuid, + latest_version_id: Uuid, + ) -> Fallible<()> { + if let Some(client) = self.0.clients.get_mut(&client_id) { + client.latest_version_id = latest_version_id; + } else { + self.0 + .clients + .insert(client_id, Client { latest_version_id }); + } + Ok(()) + } + + fn get_version_by_parent( + &mut self, + client_id: Uuid, + parent_version_id: Uuid, + ) -> Fallible> { + Ok(self + .0 + .versions + .get(&(client_id, parent_version_id)) + .cloned()) + } + + fn add_version( + &mut self, + client_id: Uuid, + version_id: Uuid, + parent_version_id: Uuid, + history_segment: Vec, + ) -> Fallible<()> { + // TODO: verify it doesn't exist (`.entry`?) + let version = Version { + version_id, + parent_version_id, + history_segment, + }; + self.0 + .versions + .insert((client_id, version.parent_version_id), version); + Ok(()) + } + + fn commit(&mut self) -> Fallible<()> { + Ok(()) + } +} diff --git a/sync-server/src/storage/mod.rs b/sync-server/src/storage/mod.rs new file mode 100644 index 000000000..2b9bb4dc0 --- /dev/null +++ b/sync-server/src/storage/mod.rs @@ -0,0 +1,56 @@ +use failure::Fallible; +use taskchampion::Uuid; + +mod inmemory; +pub(crate) use inmemory::InMemoryStorage; + +#[derive(Clone)] +pub(crate) struct Client { + pub(crate) latest_version_id: Uuid, +} + +#[derive(Clone)] +pub(crate) struct Version { + pub(crate) version_id: Uuid, + pub(crate) parent_version_id: Uuid, + pub(crate) history_segment: Vec, +} + +pub(crate) trait StorageTxn { + /// Get information about the given client + fn get_client(&mut self, client_id: Uuid) -> Fallible>; + + /// Set the client's latest_version_id (creating the client if necessary) + fn set_client_latest_version_id( + &mut self, + client_id: Uuid, + latest_version_id: Uuid, + ) -> Fallible<()>; + + /// Get a version, indexed by parent version id + fn get_version_by_parent( + &mut self, + client_id: Uuid, + parent_version_id: Uuid, + ) -> Fallible>; + + /// Add a version (that must not already exist) + fn add_version( + &mut self, + client_id: Uuid, + version_id: Uuid, + parent_version_id: Uuid, + history_segment: Vec, + ) -> Fallible<()>; + + /// Commit any changes made in the transaction. It is an error to call this more than + /// once. It is safe to skip this call for read-only operations. + fn commit(&mut self) -> Fallible<()>; +} + +/// A trait for objects able to act as storage. Most of the interesting behavior is in the +/// [`crate::storage::StorageTxn`] trait. +pub(crate) trait Storage: Send + Sync { + /// Begin a transaction + fn txn<'a>(&'a self) -> Fallible>; +} diff --git a/sync-server/src/test.rs b/sync-server/src/test.rs deleted file mode 100644 index b9f68a66d..000000000 --- a/sync-server/src/test.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::api::ServerState; -use crate::app_scope; -use crate::server::{ - AddVersionResult, ClientId, GetVersionResult, HistorySegment, SyncServer, VersionId, -}; -use actix_web::{test, App}; -use failure::Fallible; - -#[derive(Default)] -pub(crate) struct TestServer { - /// test server will panic if this is not given - pub expected_client_id: ClientId, - - pub gcv_expected_parent_version_id: VersionId, - pub gcv_result: Option, - - pub av_expected_parent_version_id: VersionId, - pub av_expected_history_segment: HistorySegment, - pub av_result: Option, -} - -impl SyncServer for TestServer { - fn get_child_version( - &self, - client_id: ClientId, - parent_version_id: VersionId, - ) -> Fallible> { - assert_eq!(client_id, self.expected_client_id); - assert_eq!(parent_version_id, self.gcv_expected_parent_version_id); - Ok(self.gcv_result.clone()) - } - - fn add_version( - &self, - client_id: ClientId, - parent_version_id: VersionId, - history_segment: HistorySegment, - ) -> Fallible { - assert_eq!(client_id, self.expected_client_id); - assert_eq!(parent_version_id, self.av_expected_parent_version_id); - assert_eq!(history_segment, self.av_expected_history_segment); - Ok(self.av_result.clone().unwrap()) - } -} - -#[actix_rt::test] -async fn test_index_get() { - let server_box: Box = Box::new(TestServer { - ..Default::default() - }); - let server_state = ServerState::new(server_box); - let mut app = test::init_service(App::new().service(app_scope(server_state))).await; - - let req = test::TestRequest::get().uri("/").to_request(); - let resp = test::call_service(&mut app, req).await; - assert!(resp.status().is_success()); -} From 5b1b911bf7db9419e366270293512002df58d080 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 26 Nov 2020 19:44:30 -0500 Subject: [PATCH 9/9] Move add_version and get_child_version to server module ..and add some tests specifically for those functions, in the absence of all the HTTP nonsense. --- sync-server/src/api/add_version.rs | 30 +--- sync-server/src/api/get_child_version.rs | 18 +-- sync-server/src/server.rs | 197 +++++++++++++++++++++++ sync-server/src/server/mod.rs | 25 --- sync-server/src/storage/mod.rs | 4 +- 5 files changed, 201 insertions(+), 73 deletions(-) create mode 100644 sync-server/src/server.rs delete mode 100644 sync-server/src/server/mod.rs diff --git a/sync-server/src/api/add_version.rs b/sync-server/src/api/add_version.rs index 2413c5fce..770ce2abc 100644 --- a/sync-server/src/api/add_version.rs +++ b/sync-server/src/api/add_version.rs @@ -2,12 +2,9 @@ use crate::api::{ failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER, }; -use crate::server::{AddVersionResult, ClientId, HistorySegment, VersionId, NO_VERSION_ID}; -use crate::storage::{Client, StorageTxn}; +use crate::server::{add_version, AddVersionResult, ClientId, VersionId}; use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result}; -use failure::Fallible; use futures::StreamExt; -use taskchampion::Uuid; /// Max history segment size: 100MB const MAX_SIZE: usize = 100 * 1024 * 1024; @@ -71,31 +68,6 @@ pub(crate) async fn service( }) } -fn add_version<'a>( - mut txn: Box, - client_id: ClientId, - client: Client, - parent_version_id: VersionId, - history_segment: HistorySegment, -) -> Fallible { - // check if this version is acceptable, under the protection of the transaction - if client.latest_version_id != NO_VERSION_ID && parent_version_id != client.latest_version_id { - return Ok(AddVersionResult::ExpectedParentVersion( - client.latest_version_id, - )); - } - - // invent a version ID - let version_id = Uuid::new_v4(); - - // update the DB - txn.add_version(client_id, version_id, parent_version_id, history_segment)?; - txn.set_client_latest_version_id(client_id, version_id)?; - txn.commit()?; - - Ok(AddVersionResult::Ok(version_id)) -} - #[cfg(test)] mod test { use crate::api::ServerState; diff --git a/sync-server/src/api/get_child_version.rs b/sync-server/src/api/get_child_version.rs index 84af0b179..83f9e2224 100644 --- a/sync-server/src/api/get_child_version.rs +++ b/sync-server/src/api/get_child_version.rs @@ -2,10 +2,8 @@ use crate::api::{ failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER, }; -use crate::server::{ClientId, GetVersionResult, VersionId}; -use crate::storage::StorageTxn; +use crate::server::{get_child_version, ClientId, VersionId}; use actix_web::{error, get, web, HttpResponse, Result}; -use failure::Fallible; /// Get a child version. /// @@ -41,20 +39,6 @@ pub(crate) async fn service( } } -fn get_child_version<'a>( - mut txn: Box, - client_id: ClientId, - parent_version_id: VersionId, -) -> Fallible> { - Ok(txn - .get_version_by_parent(client_id, parent_version_id)? - .map(|version| GetVersionResult { - version_id: version.version_id, - parent_version_id: version.parent_version_id, - history_segment: version.history_segment, - })) -} - #[cfg(test)] mod test { use crate::api::ServerState; diff --git a/sync-server/src/server.rs b/sync-server/src/server.rs new file mode 100644 index 000000000..0a404172d --- /dev/null +++ b/sync-server/src/server.rs @@ -0,0 +1,197 @@ +//! This module implements the core logic of the server: handling transactions, upholding +//! invariants, and so on. +use crate::storage::{Client, StorageTxn}; +use failure::Fallible; +use taskchampion::Uuid; + +/// The distinguished value for "no version" +pub const NO_VERSION_ID: VersionId = Uuid::nil(); + +pub(crate) type HistorySegment = Vec; +pub(crate) type ClientId = Uuid; +pub(crate) type VersionId = Uuid; + +/// Response to get_child_version +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct GetVersionResult { + pub(crate) version_id: Uuid, + pub(crate) parent_version_id: Uuid, + pub(crate) history_segment: HistorySegment, +} + +pub(crate) fn get_child_version<'a>( + mut txn: Box, + client_id: ClientId, + parent_version_id: VersionId, +) -> Fallible> { + Ok(txn + .get_version_by_parent(client_id, parent_version_id)? + .map(|version| GetVersionResult { + version_id: version.version_id, + parent_version_id: version.parent_version_id, + history_segment: version.history_segment, + })) +} + +/// Response to add_version +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum AddVersionResult { + /// OK, version added with the given ID + Ok(VersionId), + /// Rejected; expected a version with the given parent version + ExpectedParentVersion(VersionId), +} + +pub(crate) fn add_version<'a>( + mut txn: Box, + client_id: ClientId, + client: Client, + parent_version_id: VersionId, + history_segment: HistorySegment, +) -> Fallible { + // check if this version is acceptable, under the protection of the transaction + if client.latest_version_id != NO_VERSION_ID && parent_version_id != client.latest_version_id { + return Ok(AddVersionResult::ExpectedParentVersion( + client.latest_version_id, + )); + } + + // invent a version ID + let version_id = Uuid::new_v4(); + + // update the DB + txn.add_version(client_id, version_id, parent_version_id, history_segment)?; + txn.set_client_latest_version_id(client_id, version_id)?; + txn.commit()?; + + Ok(AddVersionResult::Ok(version_id)) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::storage::{InMemoryStorage, Storage}; + + #[test] + fn gcv_not_found() -> Fallible<()> { + let storage = InMemoryStorage::new(); + let txn = storage.txn()?; + let client_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + assert_eq!(get_child_version(txn, client_id, parent_version_id)?, None); + Ok(()) + } + + #[test] + fn gcv_found() -> Fallible<()> { + let storage = InMemoryStorage::new(); + let mut txn = storage.txn()?; + let client_id = Uuid::new_v4(); + let version_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let history_segment = b"abcd".to_vec(); + + txn.add_version( + client_id, + version_id, + parent_version_id, + history_segment.clone(), + )?; + + assert_eq!( + get_child_version(txn, client_id, parent_version_id)?, + Some(GetVersionResult { + version_id, + parent_version_id, + history_segment, + }) + ); + Ok(()) + } + + #[test] + fn av_conflict() -> Fallible<()> { + let storage = InMemoryStorage::new(); + let mut txn = storage.txn()?; + let client_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let history_segment = b"abcd".to_vec(); + let existing_parent_version_id = Uuid::new_v4(); + let client = Client { + latest_version_id: existing_parent_version_id, + }; + + assert_eq!( + add_version( + txn, + client_id, + client, + parent_version_id, + history_segment.clone() + )?, + AddVersionResult::ExpectedParentVersion(existing_parent_version_id) + ); + + // verify that the storage wasn't updated + txn = storage.txn()?; + assert_eq!(txn.get_client(client_id)?, None); + assert_eq!( + txn.get_version_by_parent(client_id, parent_version_id)?, + None + ); + + Ok(()) + } + + fn test_av_success(latest_version_id_nil: bool) -> Fallible<()> { + let storage = InMemoryStorage::new(); + let mut txn = storage.txn()?; + let client_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let history_segment = b"abcd".to_vec(); + let client = Client { + latest_version_id: if latest_version_id_nil { + Uuid::nil() + } else { + parent_version_id + }, + }; + + let result = add_version( + txn, + client_id, + client, + parent_version_id, + history_segment.clone(), + )?; + if let AddVersionResult::Ok(new_version_id) = result { + // check that it invented a new version ID + assert!(new_version_id != parent_version_id); + + // verify that the storage was updated + txn = storage.txn()?; + let client = txn.get_client(client_id)?.unwrap(); + assert_eq!(client.latest_version_id, new_version_id); + let version = txn + .get_version_by_parent(client_id, parent_version_id)? + .unwrap(); + assert_eq!(version.version_id, new_version_id); + assert_eq!(version.parent_version_id, parent_version_id); + assert_eq!(version.history_segment, history_segment); + } else { + panic!("did not get Ok from add_version"); + } + + Ok(()) + } + + #[test] + fn av_success_with_existing_history() -> Fallible<()> { + test_av_success(true) + } + + #[test] + fn av_success_nil_latest_version_id() -> Fallible<()> { + test_av_success(false) + } +} diff --git a/sync-server/src/server/mod.rs b/sync-server/src/server/mod.rs deleted file mode 100644 index 9e2412ba3..000000000 --- a/sync-server/src/server/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -use taskchampion::Uuid; - -/// The distinguished value for "no version" -pub const NO_VERSION_ID: VersionId = Uuid::nil(); - -pub(crate) type HistorySegment = Vec; -pub(crate) type ClientId = Uuid; -pub(crate) type VersionId = Uuid; - -/// Response to get_child_version -#[derive(Clone)] -pub(crate) struct GetVersionResult { - pub(crate) version_id: Uuid, - pub(crate) parent_version_id: Uuid, - pub(crate) history_segment: HistorySegment, -} - -/// Response to add_version -#[derive(Clone)] -pub(crate) enum AddVersionResult { - /// OK, version added with the given ID - Ok(VersionId), - /// Rejected; expected a version with the given parent version - ExpectedParentVersion(VersionId), -} diff --git a/sync-server/src/storage/mod.rs b/sync-server/src/storage/mod.rs index 2b9bb4dc0..2915f3c4d 100644 --- a/sync-server/src/storage/mod.rs +++ b/sync-server/src/storage/mod.rs @@ -4,12 +4,12 @@ use taskchampion::Uuid; mod inmemory; pub(crate) use inmemory::InMemoryStorage; -#[derive(Clone)] +#[derive(Clone, PartialEq, Debug)] pub(crate) struct Client { pub(crate) latest_version_id: Uuid, } -#[derive(Clone)] +#[derive(Clone, PartialEq, Debug)] pub(crate) struct Version { pub(crate) version_id: Uuid, pub(crate) parent_version_id: Uuid,