diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e50356b485..ab7ce2b4a4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,6 @@ jobs: path: | ~/.cargo/registry ~/.cargo/git - target key: test-cargo-${{ hashFiles('**/Cargo.toml') }} - name: Install IPFS @@ -88,7 +87,6 @@ jobs: path: | ~/.cargo/registry ~/.cargo/git - target key: rustfmt-cargo-${{ hashFiles('**/Cargo.toml') }} - name: Install dependencies @@ -121,7 +119,6 @@ jobs: path: | ~/.cargo/registry ~/.cargo/git - target key: check-cargo-${{ hashFiles('**/Cargo.toml') }} - name: Install dependencies diff --git a/Cargo.lock b/Cargo.lock index 3bfe2a84549..9aeb29a2f9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,11 +10,319 @@ dependencies = [ "regex", ] +[[package]] +name = "actix-codec" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380" +dependencies = [ + "bitflags 1.2.1", + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log 0.4.11", + "tokio 0.2.24", + "tokio-util 0.2.0", +] + +[[package]] +name = "actix-codec" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570" +dependencies = [ + "bitflags 1.2.1", + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log 0.4.11", + "pin-project 0.4.27", + "tokio 0.2.24", + "tokio-util 0.3.1", +] + +[[package]] +name = "actix-connect" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c" +dependencies = [ + "actix-codec 0.2.0", + "actix-rt", + "actix-service", + "actix-utils 1.0.6", + "derive_more", + "either", + "futures 0.3.12", + "http 0.2.3", + "log 0.4.11", + "trust-dns-proto", + "trust-dns-resolver", +] + +[[package]] +name = "actix-http" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c16664cc4fdea8030837ad5a845eb231fb93fc3c5c171edfefb52fad92ce9019" +dependencies = [ + "actix-codec 0.2.0", + "actix-connect", + "actix-rt", + "actix-service", + "actix-threadpool", + "actix-utils 1.0.6", + "base64 0.11.0", + "bitflags 1.2.1", + "brotli2", + "bytes 0.5.6", + "chrono", + "copyless", + "derive_more", + "either", + "encoding_rs", + "failure", + "flate2", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "h2 0.2.7", + "http 0.2.3", + "httparse", + "indexmap", + "language-tags", + "lazy_static", + "log 0.4.11", + "mime 0.3.16", + "percent-encoding 2.1.0", + "pin-project 0.4.27", + "rand 0.7.3", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "sha1", + "slab 0.4.2", + "time", +] + +[[package]] +name = "actix-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655" +dependencies = [ + "quote 1.0.8", + "syn 1.0.60", +] + +[[package]] +name = "actix-multipart-rfc7578" +version = "0.3.0-rc" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe270584d07ac67c6a074a963267d10be8a3457cd2183c401ba0f2880c1ec6fe" +dependencies = [ + "actix-http", + "actix-web", + "bytes 0.5.6", + "common-multipart-rfc7578", + "futures 0.3.12", +] + +[[package]] +name = "actix-router" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd1f7dbda1645bf7da33554db60891755f6c01c1b2169e2f4c492098d30c235" +dependencies = [ + "bytestring", + "http 0.2.3", + "log 0.4.11", + "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 1.4.2", + "tokio 0.2.24", +] + +[[package]] +name = "actix-server" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45407e6e672ca24784baa667c5d32ef109ccdd8d5e0b5ebb9ef8a67f4dfb708e" +dependencies = [ + "actix-codec 0.3.0", + "actix-rt", + "actix-service", + "actix-utils 2.0.0", + "futures-channel", + "futures-util", + "log 0.4.11", + "mio", + "mio-uds", + "num_cpus", + "slab 0.4.2", + "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 0.4.11", + "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 0.4.11", + "num_cpus", + "parking_lot 0.11.1", + "threadpool", +] + +[[package]] +name = "actix-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e5b4faaf105e9a6d389c606c298dcdb033061b00d532af9df56ff3a54995a8" +dependencies = [ + "actix-codec 0.2.0", + "actix-rt", + "actix-service", + "actix-utils 1.0.6", + "derive_more", + "either", + "futures 0.3.12", + "log 0.4.11", +] + +[[package]] +name = "actix-utils" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf8f5631bf01adec2267808f00e228b761c60c0584cc9fa0b5364f41d147f4e" +dependencies = [ + "actix-codec 0.2.0", + "actix-rt", + "actix-service", + "bitflags 1.2.1", + "bytes 0.5.6", + "either", + "futures 0.3.12", + "log 0.4.11", + "pin-project 0.4.27", + "slab 0.4.2", +] + +[[package]] +name = "actix-utils" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9022dec56632d1d7979e59af14f0597a28a830a9c1c7fec8b2327eb9f16b5a" +dependencies = [ + "actix-codec 0.3.0", + "actix-rt", + "actix-service", + "bitflags 1.2.1", + "bytes 0.5.6", + "either", + "futures-channel", + "futures-sink", + "futures-util", + "log 0.4.11", + "pin-project 0.4.27", + "slab 0.4.2", +] + +[[package]] +name = "actix-web" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3158e822461040822f0dbf1735b9c2ce1f95f93b651d7a7aded00b1efbb1f635" +dependencies = [ + "actix-codec 0.2.0", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-testing", + "actix-threadpool", + "actix-tls", + "actix-utils 1.0.6", + "actix-web-codegen", + "awc", + "bytes 0.5.6", + "derive_more", + "encoding_rs", + "futures 0.3.12", + "fxhash", + "log 0.4.11", + "mime 0.3.16", + "net2", + "pin-project 0.4.27", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "time", + "url 2.2.0", +] + +[[package]] +name = "actix-web-codegen" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a71bf475cbe07281d0b3696abb48212db118e7e23219f13596ce865235ff5766" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.8", + "syn 1.0.60", +] + [[package]] name = "addr2line" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" +checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" dependencies = [ "gimli 0.23.0", ] @@ -94,8 +402,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b246867b8b3b6ae56035f1eb1ed557c1d8eae97f0d53696138a50fa0e3a3b8c0" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -127,17 +435,40 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "awc" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7601d4d1d7ef2335d6597a41b5fe069f6ab799b85f53565ab390e7b7065aac5" +dependencies = [ + "actix-codec 0.2.0", + "actix-http", + "actix-rt", + "actix-service", + "base64 0.11.0", + "bytes 0.5.6", + "derive_more", + "futures-core", + "log 0.4.11", + "mime 0.3.16", + "percent-encoding 2.1.0", + "rand 0.7.3", + "serde", + "serde_json", + "serde_urlencoded", +] + [[package]] name = "backtrace" -version = "0.3.54" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2baad346b2d4e94a24347adeee9c7a93f412ee94b9cc26e5b59dea23848e9f28" +checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" dependencies = [ "addr2line", "cfg-if 1.0.0", "libc", "miniz_oxide", - "object 0.22.0", + "object 0.23.0", "rustc-demangle", ] @@ -226,7 +557,7 @@ dependencies = [ "byte-tools 0.3.1", "crypto-mac 0.7.0", "digest 0.8.1", - "opaque-debug", + "opaque-debug 0.2.3", ] [[package]] @@ -240,6 +571,17 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake2s_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e461a7034e85b211a4acb57ee2e6730b32912b06c08cc242243c39fc21ae6a2" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "blake3" version = "0.3.7" @@ -271,12 +613,22 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" dependencies = [ - "block-padding", + "block-padding 0.1.5", "byte-tools 0.3.1", "byteorder", "generic-array 0.12.3", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding 0.2.1", + "generic-array 0.14.4", +] + [[package]] name = "block-padding" version = "0.1.5" @@ -286,6 +638,32 @@ dependencies = [ "byte-tools 0.3.1", ] +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[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 = "bs58" version = "0.3.1" @@ -354,6 +732,21 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + +[[package]] +name = "bytestring" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363" +dependencies = [ + "bytes 0.5.6", +] + [[package]] name = "cc" version = "1.0.62" @@ -452,8 +845,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7471b7b1588b2cda44e0cdf3fd3da5706c1b46224dbf486c3daceb6655ec191c" dependencies = [ "bytes 0.5.6", - "futures 0.3.8", - "http 0.2.1", + "futures 0.3.12", + "http 0.2.3", "mime 0.3.16", "rand 0.5.6", ] @@ -482,22 +875,50 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "copyless" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + [[package]] name = "core-foundation" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.2", "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + [[package]] name = "core-foundation-sys" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + [[package]] name = "cranelift-bforest" version = "0.68.0" @@ -766,14 +1187,23 @@ dependencies = [ "subtle 2.3.0", ] +[[package]] +name = "ct-logs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" +dependencies = [ + "sct", +] + [[package]] name = "ctor" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fbaabec2c953050352311293be5c6aba8e141ba19d6811862b232d6fd020484" dependencies = [ - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -829,8 +1259,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -873,8 +1303,8 @@ checksum = "703e71c268ea2d8da9c0ab0b40d8b217179ee622209c170875d24443193a0dfb" dependencies = [ "heck", "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -892,8 +1322,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -1029,6 +1459,18 @@ 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 1.0.24", + "quote 1.0.8", + "syn 1.0.60", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -1164,8 +1606,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", "synstructure", ] @@ -1197,6 +1639,18 @@ dependencies = [ "log 0.4.11", ] +[[package]] +name = "filetime" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.4", + "winapi 0.3.9", +] + [[package]] name = "fixed-hash" version = "0.6.1" @@ -1221,6 +1675,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" +[[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" @@ -1297,9 +1763,9 @@ checksum = "4c7e4c2612746b0df8fed4ce0c69156021b704c9aefa360311c04e6e9e002eed" [[package]] name = "futures" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0" +checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" dependencies = [ "futures-channel", "futures-core", @@ -1312,9 +1778,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" dependencies = [ "futures-core", "futures-sink", @@ -1322,9 +1788,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" +checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" [[package]] name = "futures-cpupool" @@ -1338,9 +1804,9 @@ dependencies = [ [[package]] name = "futures-executor" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4caa2b2b68b880003057c1dd49f1ed937e38f22fcf6c212188a121f08cf40a65" +checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" dependencies = [ "futures-core", "futures-task", @@ -1349,42 +1815,42 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb" +checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" [[package]] name = "futures-macro" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" +checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" dependencies = [ "proc-macro-hack", "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] name = "futures-sink" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" +checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" [[package]] name = "futures-task" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" +checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" dependencies = [ "once_cell", ] [[package]] name = "futures-util" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" +checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" dependencies = [ "futures 0.1.30", "futures-channel", @@ -1394,13 +1860,22 @@ dependencies = [ "futures-sink", "futures-task", "memchr 2.3.4", - "pin-project 1.0.1", + "pin-project-lite 0.2.4", "pin-utils", "proc-macro-hack", "proc-macro-nested", "slab 0.4.2", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "gcc" version = "0.3.55" @@ -1435,6 +1910,36 @@ dependencies = [ "version_check 0.9.2", ] +[[package]] +name = "get_version" +version = "0.10.0" +dependencies = [ + "actix-http", + "actix-multipart-rfc7578", + "actix-rt", + "awc", + "bytes 0.5.6", + "derive_more", + "dirs 3.0.1", + "failure", + "futures 0.3.12", + "http 0.2.3", + "hyper 0.13.9", + "hyper-multipart-rfc7578", + "hyper-rustls", + "hyper-tls 0.4.3", + "ipfs-api", + "parity-multiaddr 0.9.7", + "serde", + "serde_json", + "serde_urlencoded", + "tar", + "tokio 0.2.24", + "tokio-util 0.3.1", + "typed-builder", + "walkdir", +] + [[package]] name = "getrandom" version = "0.1.15" @@ -1481,8 +1986,8 @@ dependencies = [ "chrono", "log 0.4.11", "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -1519,10 +2024,10 @@ dependencies = [ "diesel_derives", "ethabi 12.0.0-graph", "futures 0.1.30", - "futures 0.3.8", + "futures 0.3.12", "graphql-parser", "hex 0.4.2", - "http 0.2.1", + "http 0.2.3", "isatty", "lazy_static", "maplit", @@ -1552,7 +2057,7 @@ dependencies = [ "test-store", "thiserror", "tiny-keccak 1.5.0", - "tokio 0.2.23", + "tokio 0.2.24", "tokio-retry", "url 2.2.0", "uuid 0.8.1", @@ -1599,10 +2104,10 @@ dependencies = [ "anyhow", "async-trait", "atomic_refcell", - "bytes 0.5.6", + "bytes 1.0.1", "fail", "futures 0.1.30", - "futures 0.3.8", + "futures 0.3.12", "graph", "graph-graphql", "graph-mock", @@ -1612,7 +2117,6 @@ dependencies = [ "lazy_static", "lru_time_cache", "pretty_assertions", - "prometheus", "semver 0.10.0", "serde", "serde_json", @@ -1629,7 +2133,7 @@ dependencies = [ "anyhow", "crossbeam", "defer", - "futures 0.1.30", + "futures 0.3.12", "graph", "graphql-parser", "indexmap", @@ -1646,12 +2150,12 @@ dependencies = [ name = "graph-mock" version = "0.21.1" dependencies = [ - "futures 0.1.30", + "futures 0.3.12", "graph", "graph-graphql", "graphql-parser", "mockall", - "rand 0.6.5", + "rand 0.7.3", ] [[package]] @@ -1664,7 +2168,7 @@ dependencies = [ "diesel", "env_logger 0.8.2", "fail", - "futures 0.3.8", + "futures 0.3.12", "git-testament", "graph", "graph-chain-arweave", @@ -1681,7 +2185,6 @@ dependencies = [ "graphql-parser", "ipfs-api", "lazy_static", - "prometheus", "regex", "serde", "serde_regex", @@ -1695,8 +2198,8 @@ dependencies = [ name = "graph-runtime-derive" version = "0.21.1" dependencies = [ - "quote 0.6.13", - "syn 0.15.44", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -1734,12 +2237,12 @@ dependencies = [ name = "graph-server-http" version = "0.21.1" dependencies = [ - "futures 0.1.30", + "futures 0.3.12", "graph", "graph-graphql", "graph-mock", "graphql-parser", - "http 0.2.1", + "http 0.2.3", "hyper 0.13.9", "serde", ] @@ -1748,11 +2251,11 @@ dependencies = [ name = "graph-server-index-node" version = "0.21.1" dependencies = [ - "futures 0.3.8", + "futures 0.3.12", "graph", "graph-graphql", "graphql-parser", - "http 0.2.1", + "http 0.2.3", "hyper 0.13.9", "serde", ] @@ -1771,12 +2274,11 @@ dependencies = [ name = "graph-server-metrics" version = "0.21.1" dependencies = [ - "futures 0.1.30", + "futures 0.3.12", "graph", - "http 0.2.1", + "http 0.2.3", "hyper 0.13.9", "lazy_static", - "prometheus", "serde", ] @@ -1788,7 +2290,7 @@ dependencies = [ "futures 0.1.30", "graph", "graphql-parser", - "http 0.2.1", + "http 0.2.3", "lazy_static", "serde", "serde_derive", @@ -1823,7 +2325,7 @@ dependencies = [ "lru_time_cache", "maybe-owned", "postgres", - "rand 0.6.5", + "rand 0.7.3", "serde", "stable-hash", "test-store", @@ -1878,10 +2380,10 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http 0.2.1", + "http 0.2.3", "indexmap", "slab 0.4.2", - "tokio 0.2.23", + "tokio 0.2.24", "tokio-util 0.3.1", "tracing", "tracing-futures", @@ -1939,6 +2441,17 @@ dependencies = [ "digest 0.7.6", ] +[[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.1.21" @@ -1952,11 +2465,11 @@ dependencies = [ [[package]] name = "http" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" dependencies = [ - "bytes 0.5.6", + "bytes 1.0.1", "fnv", "itoa", ] @@ -1980,7 +2493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" dependencies = [ "bytes 0.5.6", - "http 0.2.1", + "http 0.2.3", ] [[package]] @@ -2070,14 +2583,14 @@ dependencies = [ "futures-core", "futures-util", "h2 0.2.7", - "http 0.2.1", + "http 0.2.3", "http-body 0.3.1", "httparse", "httpdate", "itoa", "pin-project 1.0.1", "socket2", - "tokio 0.2.23", + "tokio 0.2.24", "tower-service", "tracing", "want 0.3.0", @@ -2087,13 +2600,31 @@ dependencies = [ name = "hyper-multipart-rfc7578" version = "0.4.0-rc" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a71feb0ce26d0e28969633c06521716b07607f58f55dff84f30214b6c6d256a" +checksum = "3a71feb0ce26d0e28969633c06521716b07607f58f55dff84f30214b6c6d256a" +dependencies = [ + "bytes 0.5.6", + "common-multipart-rfc7578", + "futures 0.3.12", + "http 0.2.3", + "hyper 0.13.9", +] + +[[package]] +name = "hyper-rustls" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac965ea399ec3a25ac7d13b8affd4b8f39325cca00858ddf5eb29b79e6b14b08" dependencies = [ "bytes 0.5.6", - "common-multipart-rfc7578", - "futures 0.3.8", - "http 0.2.1", + "ct-logs", + "futures-util", "hyper 0.13.9", + "log 0.4.11", + "rustls", + "rustls-native-certs", + "tokio 0.2.24", + "tokio-rustls", + "webpki", ] [[package]] @@ -2118,7 +2649,7 @@ dependencies = [ "bytes 0.5.6", "hyper 0.13.9", "native-tls", - "tokio 0.2.23", + "tokio 0.2.24", "tokio-tls 0.3.1", ] @@ -2179,9 +2710,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" dependencies = [ "autocfg 1.0.1", "hashbrown", @@ -2215,25 +2746,37 @@ 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 0.6.2", +] + [[package]] name = "ipfs-api" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93cd854b7d68085b438401625317dc38b88a7c586cdea09ccdbe19cb49ac55b" +checksum = "f9e7fbfe5c8cb04c27fe20bf2624354f4d102652fd465a297a924c24e60aba4a" dependencies = [ "bytes 0.5.6", "dirs 2.0.2", "failure", - "futures 0.3.8", - "http 0.2.1", + "futures 0.3.12", + "http 0.2.3", "hyper 0.13.9", "hyper-multipart-rfc7578", "hyper-tls 0.4.3", - "parity-multiaddr", + "parity-multiaddr 0.7.3", "serde", "serde_json", "serde_urlencoded", - "tokio 0.2.23", + "tokio 0.2.24", "tokio-util 0.2.0", "walkdir", ] @@ -2252,7 +2795,7 @@ checksum = "e31a8281fc93ec9693494da65fbf28c0c2aa60a2eaec25dc58e2f31952e95edc" dependencies = [ "cfg-if 0.1.10", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "winapi 0.3.9", ] @@ -2382,9 +2925,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.80" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +checksum = "7eb0c4e9c72ee9d69b767adebc5f4788462a3b45624acd919475c92597bcaf4f" [[package]] name = "linked-hash-map" @@ -2428,6 +2971,15 @@ 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 = "lru_time_cache" version = "0.11.2" @@ -2449,6 +3001,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[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" @@ -2514,8 +3072,8 @@ checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" dependencies = [ "migrations_internals", "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -2618,8 +3176,8 @@ checksum = "7c461918bf7f59eefb1459252756bf2351a995d6bd510d0b2061bd86bcdabfa6" dependencies = [ "cfg-if 0.1.10", "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -2628,6 +3186,21 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0debeb9fcf88823ea64d64e4a815ab1643f33127d995978e099942ce38f25238" +[[package]] +name = "multihash" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567122ab6492f49b59def14ecc36e13e64dca4188196dd0cd41f9f3f979f3df6" +dependencies = [ + "blake2b_simd", + "blake2s_simd", + "digest 0.9.0", + "sha-1 0.9.2", + "sha2 0.9.2", + "sha3 0.9.1", + "unsigned-varint 0.5.1", +] + [[package]] name = "native-tls" version = "0.2.6" @@ -2641,8 +3214,8 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", - "security-framework-sys", + "security-framework 2.0.0", + "security-framework-sys 2.0.0", "tempfile", ] @@ -2734,9 +3307,9 @@ dependencies = [ [[package]] name = "object" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" +checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" [[package]] name = "once_cell" @@ -2750,6 +3323,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.30" @@ -2822,7 +3401,25 @@ dependencies = [ "percent-encoding 2.1.0", "serde", "static_assertions", - "unsigned-varint", + "unsigned-varint 0.3.3", + "url 2.2.0", +] + +[[package]] +name = "parity-multiaddr" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbecae7b7cfaafea47ceb5253ecdd14155ca8410e3487ed86031a6c2d5c15873" +dependencies = [ + "arrayref", + "bs58 0.4.0", + "byteorder", + "data-encoding", + "multihash", + "percent-encoding 2.1.0", + "serde", + "static_assertions", + "unsigned-varint 0.5.1", "url 2.2.0", ] @@ -2835,10 +3432,10 @@ dependencies = [ "blake2", "bytes 0.5.6", "rand 0.7.3", - "sha-1", + "sha-1 0.8.2", "sha2 0.8.2", - "sha3", - "unsigned-varint", + "sha3 0.8.2", + "unsigned-varint 0.3.3", ] [[package]] @@ -2894,7 +3491,7 @@ dependencies = [ "cfg-if 0.1.10", "cloudabi 0.0.3", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "rustc_version", "smallvec 0.6.13", "winapi 0.3.9", @@ -2909,7 +3506,7 @@ dependencies = [ "cfg-if 0.1.10", "cloudabi 0.0.3", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "smallvec 1.4.2", "winapi 0.3.9", ] @@ -2924,7 +3521,7 @@ dependencies = [ "cloudabi 0.1.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "smallvec 1.4.2", "winapi 0.3.9", ] @@ -3004,8 +3601,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -3015,8 +3612,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81a4ffa594b66bff340084d4081df649a7dc049ac8d7fc458d8e628bfbbb2f86" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -3025,6 +3622,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" +[[package]] +name = "pin-project-lite" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" + [[package]] name = "pin-utils" version = "0.1.0" @@ -3169,8 +3772,8 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", "version_check 0.9.2", ] @@ -3181,7 +3784,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", + "quote 1.0.8", "version_check 0.9.2", ] @@ -3217,16 +3820,17 @@ dependencies = [ [[package]] name = "prometheus" -version = "0.7.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5567486d5778e2c6455b1b90ff1c558f29e751fc018130fa182e15828e728af1" +checksum = "c8425533e7122f0c3cc7a37e6244b16ad3a2cc32ae7ac6276e2a75da0d9c200d" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "fnv", "lazy_static", + "parking_lot 0.11.1", "protobuf", - "quick-error", - "spin", + "regex", + "thiserror", ] [[package]] @@ -3261,9 +3865,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" dependencies = [ "proc-macro2 1.0.24", ] @@ -3519,6 +4123,15 @@ version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +[[package]] +name = "redox_syscall" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +dependencies = [ + "bitflags 1.2.1", +] + [[package]] name = "redox_users" version = "0.3.5" @@ -3526,7 +4139,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.1.57", "rust-argon2", ] @@ -3597,7 +4210,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "http 0.2.1", + "http 0.2.3", "http-body 0.3.1", "hyper 0.13.9", "hyper-tls 0.4.3", @@ -3609,16 +4222,41 @@ dependencies = [ "mime_guess", "native-tls", "percent-encoding 2.1.0", - "pin-project-lite", + "pin-project-lite 0.1.11", "serde", "serde_urlencoded", - "tokio 0.2.23", + "tokio 0.2.24", "tokio-tls 0.3.1", "url 2.2.0", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.7.0", +] + +[[package]] +name = "resolv-conf" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11834e137f3b14e309437a8276714eed3a80d1ef894869e510f2c0c0b98b9f4a" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "ring" +version = "0.16.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", ] [[package]] @@ -3669,6 +4307,31 @@ dependencies = [ "semver 0.9.0", ] +[[package]] +name = "rustls" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1" +dependencies = [ + "base64 0.11.0", + "log 0.4.11", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75ffeb84a6bd9d014713119542ce415db3a3e4748f0bfce1e1416cd224a23a5" +dependencies = [ + "openssl-probe", + "rustls", + "schannel", + "security-framework 0.4.4", +] + [[package]] name = "ryu" version = "1.0.5" @@ -3743,8 +4406,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b12bd20b94c7cdfda8c7ba9b92ad0d9a56e3fa018c25fca83b51aa664c9b4c0d" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", +] + +[[package]] +name = "sct" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -3763,6 +4436,19 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" +dependencies = [ + "bitflags 1.2.1", + "core-foundation 0.7.0", + "core-foundation-sys 0.7.0", + "libc", + "security-framework-sys 0.4.3", +] + [[package]] name = "security-framework" version = "2.0.0" @@ -3770,10 +4456,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" dependencies = [ "bitflags 1.2.1", - "core-foundation", - "core-foundation-sys", + "core-foundation 0.9.1", + "core-foundation-sys 0.8.2", + "libc", + "security-framework-sys 2.0.0", +] + +[[package]] +name = "security-framework-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" +dependencies = [ + "core-foundation-sys 0.7.0", "libc", - "security-framework-sys", ] [[package]] @@ -3782,7 +4478,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.2", "libc", ] @@ -3812,29 +4508,29 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.117" +version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.117" +version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] name = "serde_json" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" dependencies = [ "itoa", "ryu", @@ -3884,7 +4580,20 @@ dependencies = [ "block-buffer 0.7.3", "digest 0.8.1", "fake-simd", - "opaque-debug", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha-1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -3914,7 +4623,20 @@ dependencies = [ "block-buffer 0.7.3", "digest 0.8.1", "fake-simd", - "opaque-debug", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -3927,7 +4649,19 @@ dependencies = [ "byte-tools 0.3.1", "digest 0.8.1", "keccak", - "opaque-debug", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug 0.3.0", ] [[package]] @@ -3949,6 +4683,15 @@ dependencies = [ "dirs 2.0.2", ] +[[package]] +name = "signal-hook-registry" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "0.2.3" @@ -4058,16 +4801,10 @@ checksum = "7fd8b795c389288baa5f355489c65e71fd48a02104600d15c4cfbc561e9e429d" dependencies = [ "cfg-if 0.1.10", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "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 = "stable-hash" version = "0.3.0" @@ -4148,8 +4885,8 @@ dependencies = [ "heck", "proc-macro-error", "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -4166,8 +4903,8 @@ checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" dependencies = [ "heck", "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -4195,12 +4932,12 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.48" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", + "quote 1.0.8", "unicode-xid 0.2.1", ] @@ -4211,8 +4948,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", "unicode-xid 0.2.1", ] @@ -4222,6 +4959,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" +[[package]] +name = "tar" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0313546c01d59e29be4f09687bcb4fb6690cec931cc3607b6aec7a0e417f4cc6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.11.1" @@ -4237,7 +4985,7 @@ dependencies = [ "cfg-if 0.1.10", "libc", "rand 0.7.3", - "redox_syscall", + "redox_syscall 0.1.57", "remove_dir_all", "winapi 0.3.9", ] @@ -4301,8 +5049,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -4314,6 +5062,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" @@ -4384,9 +5141,9 @@ dependencies = [ [[package]] name = "tokio" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff" +checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48" dependencies = [ "bytes 0.5.6", "fnv", @@ -4398,9 +5155,11 @@ dependencies = [ "mio", "mio-uds", "num_cpus", - "pin-project-lite", + "pin-project-lite 0.1.11", + "signal-hook-registry", "slab 0.4.2", "tokio-macros", + "winapi 0.3.9", ] [[package]] @@ -4493,8 +5252,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", ] [[package]] @@ -4522,9 +5281,21 @@ version = "0.2.0" source = "git+https://github.com/graphprotocol/rust-tokio-retry?branch=update-to-tokio-02#fc0e868bdc7ff79ac84cf502d4cec20c5ce5adaf" dependencies = [ "futures 0.1.30", - "futures 0.3.8", + "futures 0.3.12", "rand 0.4.6", - "tokio 0.2.23", + "tokio 0.2.24", +] + +[[package]] +name = "tokio-rustls" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cb62a0d2770787abc96e99c1cd98fcf17f94959f3af63ca85bdfb203f051b4" +dependencies = [ + "futures-core", + "rustls", + "tokio 0.2.24", + "webpki", ] [[package]] @@ -4608,7 +5379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" dependencies = [ "native-tls", - "tokio 0.2.23", + "tokio 0.2.24", ] [[package]] @@ -4617,10 +5388,10 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8b8fe88007ebc363512449868d7da4389c9400072a3f666f212c7280082882a" dependencies = [ - "futures 0.3.8", + "futures 0.3.12", "log 0.4.11", "pin-project 0.4.27", - "tokio 0.2.23", + "tokio 0.2.24", "tungstenite", ] @@ -4684,8 +5455,8 @@ dependencies = [ "futures-core", "futures-sink", "log 0.4.11", - "pin-project-lite", - "tokio 0.2.23", + "pin-project-lite 0.1.11", + "tokio 0.2.24", ] [[package]] @@ -4698,8 +5469,8 @@ dependencies = [ "futures-core", "futures-sink", "log 0.4.11", - "pin-project-lite", - "tokio 0.2.23", + "pin-project-lite 0.1.11", + "tokio 0.2.24", ] [[package]] @@ -4725,7 +5496,7 @@ checksum = "b0987850db3733619253fe60e17cb59b82d37c7e6c0236bb81e4d6b87c879f27" dependencies = [ "cfg-if 0.1.10", "log 0.4.11", - "pin-project-lite", + "pin-project-lite 0.1.11", "tracing-core", ] @@ -4760,6 +5531,45 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" +[[package]] +name = "trust-dns-proto" +version = "0.18.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7f3a2ab8a919f5eca52a468866a67ed7d3efa265d48a652a9a3452272b413f" +dependencies = [ + "async-trait", + "enum-as-inner", + "failure", + "futures 0.3.12", + "idna 0.2.0", + "lazy_static", + "log 0.4.11", + "rand 0.7.3", + "smallvec 1.4.2", + "socket2", + "tokio 0.2.24", + "url 2.2.0", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.18.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f90b1502b226f8b2514c6d5b37bafa8c200d7ca4102d57dc36ee0f3b7a04a2f" +dependencies = [ + "cfg-if 0.1.10", + "failure", + "futures 0.3.12", + "ipconfig", + "lazy_static", + "log 0.4.11", + "lru-cache", + "resolv-conf", + "smallvec 1.4.2", + "tokio 0.2.24", + "trust-dns-proto", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -4775,12 +5585,12 @@ dependencies = [ "base64 0.11.0", "byteorder", "bytes 0.5.6", - "http 0.2.1", + "http 0.2.3", "httparse", "input_buffer", "log 0.4.11", "rand 0.7.3", - "sha-1", + "sha-1 0.8.2", "url 2.2.0", "utf-8", ] @@ -4791,6 +5601,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" +[[package]] +name = "typed-builder" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fc4459191c621a53ef6c6ca5642e6e0e5ccc61f3e5b8ad6b6ab5317f0200fb" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.8", + "syn 1.0.60", +] + [[package]] name = "typenum" version = "1.12.0" @@ -4884,6 +5705,18 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f67332660eb59a6f1eb24ff1220c9e8d01738a8503c6002e30bcfe4bd9f2b4a9" +[[package]] +name = "unsigned-varint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fdeedbf205afadfe39ae559b75c3240f24e257d0ca27e85f85cb82aa19ac35" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "1.7.2" @@ -5027,8 +5860,8 @@ dependencies = [ "lazy_static", "log 0.4.11", "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", "wasm-bindgen-shared", ] @@ -5050,7 +5883,7 @@ version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" dependencies = [ - "quote 1.0.7", + "quote 1.0.8", "wasm-bindgen-macro-support", ] @@ -5061,8 +5894,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.48", + "quote 1.0.8", + "syn 1.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5336,6 +6169,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "websocket" version = "0.21.1" @@ -5358,6 +6201,12 @@ dependencies = [ "url 1.7.2", ] +[[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" @@ -5401,6 +6250,15 @@ 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 = "winreg" version = "0.7.0" @@ -5420,6 +6278,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + [[package]] name = "yaml-rust" version = "0.4.4" diff --git a/Cargo.toml b/Cargo.toml index 4e4d8a405c0..894508b6bc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,10 @@ members = [ "store/test-store", "graph", "tests", + "example", ] [patch.crates-io] # Include protection against stack overflow when parsing from this PR: https://github.com/graphql-rust/graphql-parser/commit/45167b53e9533c331298683577ba8df7e43480ac -graphql-parser = {git="https://github.com/graphql-rust/graphql-parser", commit="45167b53e9533c331298683577ba8df7e43480ac"} \ No newline at end of file +graphql-parser = {git="https://github.com/graphql-rust/graphql-parser", commit="45167b53e9533c331298683577ba8df7e43480ac"} diff --git a/chain/ethereum/Cargo.toml b/chain/ethereum/Cargo.toml index 3ee8475b622..676c0fceaae 100644 --- a/chain/ethereum/Cargo.toml +++ b/chain/ethereum/Cargo.toml @@ -9,11 +9,11 @@ futures = "0.1.21" hyper = "0.12.25" # must be compatible with the version rust-web3 uses jsonrpc-core = "14.2.0" graph = { path = "../../graph" } -lazy_static = "1.2.0" +lazy_static = "1.4.0" state_machine_future = "0.2" -serde = "1.0" +serde = "1.0.123" config = { version = "0.10", features = ["toml"], default-features = false } -dirs = "3.0" +dirs = "3.0.1" anyhow = "1.0" fail = "0.4" diff --git a/chain/ethereum/src/block_stream.rs b/chain/ethereum/src/block_stream.rs index b760ad053c6..a682a204560 100644 --- a/chain/ethereum/src/block_stream.rs +++ b/chain/ethereum/src/block_stream.rs @@ -1,10 +1,11 @@ use std::cmp; -use std::collections::{HashMap, VecDeque}; +use std::collections::VecDeque; use std::mem; use std::time::Duration; -use graph::components::ethereum::{ - blocks_with_triggers, triggers_in_block, EthereumNetworks, NodeCapabilities, +use graph::components::{ + ethereum::{blocks_with_triggers, triggers_in_block, EthereumNetworks, NodeCapabilities}, + store::BlockStore, }; use graph::prelude::{ BlockStream as BlockStreamTrait, BlockStreamBuilder as BlockStreamBuilderTrait, *, @@ -22,7 +23,7 @@ lazy_static! { /// Ideal number of triggers in a range. The range size will adapt to try to meet this. static ref TARGET_TRIGGERS_PER_BLOCK_RANGE: u64 = std::env::var("GRAPH_ETHEREUM_TARGET_TRIGGERS_PER_BLOCK_RANGE") - .unwrap_or("100".into()) + .unwrap_or("2000".into()) .parse::() .expect("invalid GRAPH_ETHEREUM_TARGET_TRIGGERS_PER_BLOCK_RANGE"); } @@ -140,7 +141,7 @@ enum NextBlocks { impl BlockStream where - S: Store, + S: SubgraphStore, C: ChainStore, { pub fn new( @@ -188,7 +189,7 @@ where impl BlockStreamContext where - S: Store, + S: SubgraphStore, C: ChainStore, { /// Perform reconciliation steps until there are blocks to yield or we are up-to-date. @@ -556,9 +557,9 @@ where } } -impl BlockStreamTrait for BlockStream {} +impl BlockStreamTrait for BlockStream {} -impl Stream for BlockStream { +impl Stream for BlockStream { type Item = BlockStreamEvent; type Error = Error; @@ -720,20 +721,20 @@ impl Stream for BlockStream { } } -pub struct BlockStreamBuilder { +pub struct BlockStreamBuilder { subgraph_store: Arc, - chain_stores: HashMap>, + block_store: Arc, eth_networks: EthereumNetworks, node_id: NodeId, reorg_threshold: u64, metrics_registry: Arc, } -impl Clone for BlockStreamBuilder { +impl Clone for BlockStreamBuilder { fn clone(&self) -> Self { BlockStreamBuilder { subgraph_store: self.subgraph_store.clone(), - chain_stores: self.chain_stores.clone(), + block_store: self.block_store.clone(), eth_networks: self.eth_networks.clone(), node_id: self.node_id.clone(), reorg_threshold: self.reorg_threshold, @@ -742,15 +743,15 @@ impl Clone for BlockStreamBuilder { } } -impl BlockStreamBuilder +impl BlockStreamBuilder where - S: Store, - C: ChainStore, + S: SubgraphStore, + B: BlockStore, M: MetricsRegistry, { pub fn new( subgraph_store: Arc, - chain_stores: HashMap>, + block_store: Arc, eth_networks: EthereumNetworks, node_id: NodeId, reorg_threshold: u64, @@ -758,7 +759,7 @@ where ) -> Self { BlockStreamBuilder { subgraph_store, - chain_stores, + block_store, eth_networks, node_id, reorg_threshold, @@ -767,13 +768,13 @@ where } } -impl BlockStreamBuilderTrait for BlockStreamBuilder +impl BlockStreamBuilderTrait for BlockStreamBuilder where - S: Store, - C: ChainStore, + S: SubgraphStore, + B: BlockStore, M: MetricsRegistry, { - type Stream = BlockStream; + type Stream = BlockStream; fn build( &self, @@ -792,8 +793,8 @@ where )); let chain_store = self - .chain_stores - .get(&network_name) + .block_store + .chain_store(&network_name) .expect(&format!( "no store that supports network: {}", &network_name diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 0de1689bdfe..03cefe672f8 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -43,7 +43,7 @@ lazy_static! { .expect("invalid number of parallel Ethereum block ranges to scan"); static ref BLOCK_BATCH_SIZE: usize = std::env::var("ETHEREUM_BLOCK_BATCH_SIZE") - .unwrap_or("10".into()) + .unwrap_or("100".into()) .parse::() .expect("invalid ETHEREUM_BLOCK_BATCH_SIZE env var"); diff --git a/chain/ethereum/src/network_indexer/block_writer.rs b/chain/ethereum/src/network_indexer/block_writer.rs index 24f5b83183c..6f8b4a3e7c9 100644 --- a/chain/ethereum/src/network_indexer/block_writer.rs +++ b/chain/ethereum/src/network_indexer/block_writer.rs @@ -43,7 +43,7 @@ pub struct BlockWriter { logger: Logger, /// Store that manages the network subgraph. - store: Arc, + store: Arc, /// Metrics for analyzing the block writer performance. metrics: Arc, @@ -54,7 +54,7 @@ impl BlockWriter { pub fn new( subgraph_id: SubgraphDeploymentId, logger: &Logger, - store: Arc, + store: Arc, stopwatch: StopwatchMetrics, metrics_registry: Arc, ) -> Self { @@ -97,7 +97,7 @@ impl BlockWriter { struct WriteContext { logger: Logger, subgraph_id: SubgraphDeploymentId, - store: Arc, + store: Arc, cache: EntityCache, metrics: Arc, } diff --git a/chain/ethereum/src/network_indexer/mod.rs b/chain/ethereum/src/network_indexer/mod.rs index f3eca8712aa..5989f705c89 100644 --- a/chain/ethereum/src/network_indexer/mod.rs +++ b/chain/ethereum/src/network_indexer/mod.rs @@ -83,6 +83,6 @@ impl fmt::Display for BlockWithOmmers { } } -pub trait NetworkStore: Store + ChainStore {} +pub trait NetworkStore: SubgraphStore + ChainStore {} -impl NetworkStore for S {} +impl NetworkStore for S {} diff --git a/chain/ethereum/src/network_indexer/network_indexer.rs b/chain/ethereum/src/network_indexer/network_indexer.rs index 4f488910d0c..04952e4dc77 100644 --- a/chain/ethereum/src/network_indexer/network_indexer.rs +++ b/chain/ethereum/src/network_indexer/network_indexer.rs @@ -81,7 +81,7 @@ macro_rules! track_future { fn ensure_subgraph( logger: Logger, - store: Arc, + store: Arc, subgraph_name: SubgraphName, subgraph_id: SubgraphDeploymentId, start_block: Option, @@ -447,7 +447,7 @@ fn update_chain_and_local_head_metrics( pub struct Context { logger: Logger, adapter: Arc, - store: Arc, + store: Arc, metrics: Arc, block_writer: Arc, event_sink: Sender, @@ -1121,7 +1121,7 @@ impl NetworkIndexer { network_name: String, ) -> Self where - S: Store + ChainStore, + S: SubgraphStore, { // Create a subgraph name and ID let id_str = format!( diff --git a/chain/ethereum/src/network_indexer/subgraph.rs b/chain/ethereum/src/network_indexer/subgraph.rs index f1634edacf7..504d495e4ef 100644 --- a/chain/ethereum/src/network_indexer/subgraph.rs +++ b/chain/ethereum/src/network_indexer/subgraph.rs @@ -3,14 +3,14 @@ use futures::future::FutureResult; use std::collections::BTreeSet; fn check_subgraph_exists( - store: Arc, + store: Arc, subgraph_id: SubgraphDeploymentId, ) -> impl Future { future::result(store.is_deployed(&subgraph_id)) } fn create_subgraph( - store: Arc, + store: Arc, subgraph_name: SubgraphName, subgraph_id: SubgraphDeploymentId, start_block: Option, @@ -50,7 +50,7 @@ pub fn ensure_subgraph_exists( subgraph_name: SubgraphName, subgraph_id: SubgraphDeploymentId, logger: Logger, - store: Arc, + store: Arc, start_block: Option, network_name: String, ) -> impl Future { diff --git a/chain/ethereum/src/transport.rs b/chain/ethereum/src/transport.rs index bac3d57d6ed..00780f0c58f 100644 --- a/chain/ethereum/src/transport.rs +++ b/chain/ethereum/src/transport.rs @@ -40,7 +40,7 @@ impl Transport { pub fn new_rpc(rpc: &str) -> (EventLoopHandle, Self) { let max_parallel_http: usize = env::var_os("ETHEREUM_RPC_MAX_PARALLEL_REQUESTS") .map(|s| s.to_str().unwrap().parse().unwrap()) - .unwrap_or(64); + .unwrap_or(128); let cfg = ETHEREUM_CONFIG.rpc.get(rpc); let headers = cfg.map(|cfg| cfg.http_headers.clone()).unwrap_or_default(); diff --git a/chain/ethereum/tests/network_indexer.rs b/chain/ethereum/tests/network_indexer.rs index 1a518c4599b..a0f26aff0e4 100644 --- a/chain/ethereum/tests/network_indexer.rs +++ b/chain/ethereum/tests/network_indexer.rs @@ -12,7 +12,7 @@ use graph_chain_ethereum::network_indexer::{ self as network_indexer, BlockWithOmmers, NetworkIndexerEvent, }; use graph_core::MetricsRegistry; -use graph_store_postgres::NetworkStore as DieselStore; +use graph_store_postgres::Store as DieselStore; use web3::types::{H2048, H256, H64, U256}; use test_store::*; @@ -35,6 +35,7 @@ macro_rules! revert { // Helper to wipe the store clean. fn remove_test_data(store: Arc) { store + .subgraph_store() .delete_all_entities_for_test_use_only() .expect("removing test data succeeds"); } @@ -64,7 +65,7 @@ fn run_network_indexer( let mut indexer = network_indexer::NetworkIndexer::new( &logger, adapter, - store.clone(), + store.subgraph_store(), metrics_registry, subgraph_name.to_string(), start_block, diff --git a/core/Cargo.toml b/core/Cargo.toml index 176af7b181e..196907be200 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -6,17 +6,17 @@ edition = "2018" [dependencies] async-trait = "0.1.41" atomic_refcell = "0.1.6" -bytes = "0.5" +bytes = "1.0.1" futures01 = { package="futures", version="0.1.29" } -futures = { version="0.3.4", features=["compat"] } +futures = { version="0.3.12", features=["compat"] } graph = { path = "../graph" } graph-graphql = { path = "../graphql" } -ipfs-api = { version = "=0.7.1", features = ["hyper-tls"] } -lazy_static = "1.2.0" +ipfs-api = { version = "=0.7.2", features = ["hyper-tls"] } +lazy_static = "1.4.0" lru_time_cache = "0.11" semver = "0.10.0" serde = "1.0" -serde_json = "1.0" +serde_json = "1.0.61" serde_yaml = "0.8" fail = "0.4" @@ -26,6 +26,5 @@ walkdir = "2.3.1" test-store = { path = "../store/test-store" } hex = "0.4.2" graphql-parser = "0.3" -prometheus = "0.7" pretty_assertions = "0.6.1" -anyhow = "1.0" \ No newline at end of file +anyhow = "1.0" diff --git a/core/src/metrics/registry.rs b/core/src/metrics/registry.rs index 283865f2d2e..665ce84a700 100644 --- a/core/src/metrics/registry.rs +++ b/core/src/metrics/registry.rs @@ -97,12 +97,12 @@ impl MetricsRegistryTrait for MetricsRegistry { "registering metric [{}] because it was already registered", name, ); } - PrometheusError::InconsistentCardinality(expected, got) => { + PrometheusError::InconsistentCardinality { expect, got } => { error!( self.logger, "registering metric [{}] failed due to inconsistent caridinality, expected = {} got = {}", name, - expected, + expect, got, ); } diff --git a/core/src/subgraph/instance_manager.rs b/core/src/subgraph/instance_manager.rs index 96626559928..09cb18a08b8 100644 --- a/core/src/subgraph/instance_manager.rs +++ b/core/src/subgraph/instance_manager.rs @@ -6,7 +6,7 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use std::sync::{Arc, RwLock}; use std::time::Instant; -use graph::components::store::ModificationsAndCache; +use graph::components::store::{BlockStore, ModificationsAndCache}; use graph::components::subgraph::{MappingError, ProofOfIndexing, SharedProofOfIndexing}; use graph::components::{ ethereum::{triggers_in_block, EthereumNetworks}, @@ -39,12 +39,13 @@ lazy_static! { type SharedInstanceKeepAliveMap = Arc>>; -struct IndexingInputs { +struct IndexingInputs { deployment_id: SubgraphDeploymentId, features: BTreeSet, network_name: String, start_blocks: Vec, store: Arc, + chain_store: Arc, eth_adapter: Arc, stream_builder: B, include_calls_in_blocks: bool, @@ -61,9 +62,9 @@ struct IndexingState { entity_lfu_cache: LfuCache>, } -struct IndexingContext { +struct IndexingContext { /// Read only inputs that are needed while indexing a subgraph. - pub inputs: IndexingInputs, + pub inputs: IndexingInputs, /// Mutable state that may be modified while indexing a subgraph. pub state: IndexingState, @@ -186,16 +187,18 @@ impl SubgraphInstanceMetrics { impl SubgraphInstanceManager { /// Creates a new runtime manager. - pub fn new( + pub fn new( logger_factory: &LoggerFactory, - stores: HashMap>, + store: Arc, + block_store: Arc, eth_networks: EthereumNetworks, host_builder: impl RuntimeHostBuilder, block_stream_builder: B, metrics_registry: Arc, ) -> Self where - S: Store + ChainStore + EthereumCallCache, + S: SubgraphStore, + BS: BlockStore, B: BlockStreamBuilder, M: MetricsRegistry, { @@ -209,7 +212,8 @@ impl SubgraphInstanceManager { Self::handle_subgraph_events( logger_factory, subgraph_receiver, - stores, + store, + block_store, eth_networks, host_builder, block_stream_builder, @@ -223,16 +227,18 @@ impl SubgraphInstanceManager { } /// Handle incoming events from subgraph providers. - fn handle_subgraph_events( + fn handle_subgraph_events( logger_factory: LoggerFactory, receiver: Receiver, - stores: HashMap>, + store: Arc, + block_store: Arc, eth_networks: EthereumNetworks, host_builder: impl RuntimeHostBuilder, block_stream_builder: B, metrics_registry: Arc, ) where - S: Store + ChainStore + EthereumCallCache, + S: SubgraphStore, + BS: BlockStore, B: BlockStreamBuilder, M: MetricsRegistry, { @@ -266,7 +272,8 @@ impl SubgraphInstanceManager { instances.clone(), host_builder.clone(), block_stream_builder.clone(), - stores.get(&network).cloned(), + store.clone(), + block_store.chain_store(&network), ð_networks, manifest, metrics_registry_for_subgraph.clone(), @@ -294,26 +301,32 @@ impl SubgraphInstanceManager { }); } - async fn start_subgraph( + async fn start_subgraph( logger: Logger, instances: SharedInstanceKeepAliveMap, host_builder: impl RuntimeHostBuilder, stream_builder: B, - store: Option>, + store: Arc, + chain_store: Option>, eth_networks: &EthereumNetworks, manifest: SubgraphManifest, registry: Arc, ) -> Result<(), Error> where B: BlockStreamBuilder, - S: Store + ChainStore + EthereumCallCache, + S: SubgraphStore, + C: ChainStore, M: MetricsRegistry, { let required_capabilities = manifest.required_ethereum_capabilities(); let network = manifest.network_name(); - let store = store - .ok_or_else(|| anyhow!("expected store that matches subgraph network: {}", &network))?; + let chain_store = chain_store.ok_or_else(|| { + anyhow!( + "expected chain store that matches subgraph network: {}", + &network + ) + })?; let eth_adapter = eth_networks .adapter_with_capabilities(network.clone(), &required_capabilities).map_err(|e| @@ -373,6 +386,7 @@ impl SubgraphInstanceManager { features, network_name, start_blocks, + chain_store, store, eth_adapter, stream_builder, @@ -429,17 +443,19 @@ impl EventConsumer for SubgraphInstanceManager } } -async fn run_subgraph(mut ctx: IndexingContext) -> Result<(), ()> +async fn run_subgraph(mut ctx: IndexingContext) -> Result<(), ()> where B: BlockStreamBuilder, T: RuntimeHostBuilder, - S: ChainStore + Store + EthereumCallCache, + S: SubgraphStore, + C: ChainStore, { // Clone a few things for different parts of the async processing let subgraph_metrics = ctx.subgraph_metrics.cheap_clone(); let store_for_err = ctx.inputs.store.cheap_clone(); let logger = ctx.state.logger.cheap_clone(); let id_for_err = ctx.inputs.deployment_id.clone(); + let mut first_run = true; loop { debug!(logger, "Starting or restarting subgraph"); @@ -493,7 +509,7 @@ where .eth_adapter .load_blocks( logger.cheap_clone(), - ctx.inputs.store.cheap_clone(), + ctx.inputs.chain_store.cheap_clone(), HashSet::from_iter(Some(subgraph_ptr.hash)), ) .collect() @@ -581,6 +597,18 @@ where match res { Ok((c, needs_restart)) => { ctx = c; + + // Unfail the subgraph if it was previously failed. + // As an optimization we check this only on the first run. + if first_run { + first_run = false; + + ctx.inputs + .store + .unfail(&ctx.inputs.deployment_id) + .map_err(|_| ())?; + } + if needs_restart { // Cancel the stream for real ctx.state @@ -665,15 +693,16 @@ impl From for BlockProcessingError { /// Processes a block and returns the updated context and a boolean flag indicating /// whether new dynamic data sources have been added to the subgraph. -async fn process_block( +async fn process_block( logger: &Logger, eth_adapter: Arc, - mut ctx: IndexingContext, + mut ctx: IndexingContext, block_stream_cancel_handle: CancelHandle, block: EthereumBlockWithTriggers, -) -> Result<(IndexingContext, bool), BlockProcessingError> +) -> Result<(IndexingContext, bool), BlockProcessingError> where - S: ChainStore + Store + EthereumCallCache, + S: SubgraphStore, + C: ChainStore, { let triggers = block.triggers; let block = block.ethereum_block; @@ -795,7 +824,7 @@ where let block_with_triggers = triggers_in_block( eth_adapter.clone(), logger.cheap_clone(), - ctx.inputs.store.clone(), + ctx.inputs.chain_store.clone(), ctx.ethrpc_metrics.clone(), EthereumLogFilter::from_data_sources(data_sources.iter()), EthereumCallFilter::from_data_sources(data_sources.iter()), @@ -1039,15 +1068,16 @@ async fn process_triggers( Ok(block_state) } -fn create_dynamic_data_sources( +fn create_dynamic_data_sources( logger: Logger, - ctx: &mut IndexingContext, + ctx: &mut IndexingContext, host_metrics: Arc, created_data_sources: Vec, ) -> Result<(Vec, Vec>), Error> where B: BlockStreamBuilder, - S: ChainStore + Store + EthereumCallCache, + S: SubgraphStore, + C: ChainStore, { let mut data_sources = vec![]; let mut runtime_hosts = vec![]; @@ -1088,15 +1118,16 @@ where Ok((data_sources, runtime_hosts)) } -fn persist_dynamic_data_sources( +fn persist_dynamic_data_sources( logger: Logger, - ctx: &mut IndexingContext, + ctx: &mut IndexingContext, entity_cache: &mut EntityCache, data_sources: Vec, block_ptr: EthereumBlockPointer, ) where B: BlockStreamBuilder, - S: ChainStore + Store, + S: SubgraphStore, + C: ChainStore, { if !data_sources.is_empty() { debug!( diff --git a/core/src/subgraph/loader.rs b/core/src/subgraph/loader.rs index 9e1423dbd95..f66b5c6575f 100644 --- a/core/src/subgraph/loader.rs +++ b/core/src/subgraph/loader.rs @@ -13,7 +13,7 @@ pub struct DataSourceLoader { impl DataSourceLoader where - S: Store, + S: SubgraphStore, { pub fn new(store: Arc) -> Self { Self { store } @@ -23,7 +23,7 @@ where #[async_trait] impl DataSourceLoaderTrait for DataSourceLoader where - S: Store, + S: SubgraphStore, { async fn load_dynamic_data_sources( &self, diff --git a/core/src/subgraph/provider.rs b/core/src/subgraph/provider.rs index 317c56bb50c..161e04da05a 100644 --- a/core/src/subgraph/provider.rs +++ b/core/src/subgraph/provider.rs @@ -24,7 +24,7 @@ pub struct SubgraphAssignmentProvider { impl SubgraphAssignmentProvider where L: LinkResolver + Clone, - S: Store, + S: SubgraphStore, { pub fn new(logger_factory: &LoggerFactory, resolver: Arc, store: Arc) -> Self { let (event_sink, event_stream) = channel(100); @@ -54,7 +54,7 @@ where impl SubgraphAssignmentProviderTrait for SubgraphAssignmentProvider where L: LinkResolver + Clone, - S: Store, + S: SubgraphStore, { async fn start( &self, diff --git a/core/src/subgraph/registrar.rs b/core/src/subgraph/registrar.rs index b99ad95686f..5e4e80fd152 100644 --- a/core/src/subgraph/registrar.rs +++ b/core/src/subgraph/registrar.rs @@ -1,12 +1,12 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::env; use std::time::{Duration, Instant}; use async_trait::async_trait; use lazy_static::lazy_static; -use graph::components::ethereum::EthereumNetworks; use graph::components::store::SubscriptionManager; +use graph::components::{ethereum::EthereumNetworks, store::BlockStore}; use graph::data::subgraph::schema::SubgraphDeploymentEntity; use graph::prelude::{ CreateSubgraphResult, SubgraphAssignmentProvider as SubgraphAssignmentProviderTrait, @@ -23,26 +23,26 @@ lazy_static! { ); } -pub struct SubgraphRegistrar { +pub struct SubgraphRegistrar { logger: Logger, logger_factory: LoggerFactory, resolver: Arc, provider: Arc

, store: Arc, subscription_manager: Arc, - chain_stores: HashMap>, + block_store: Arc, ethereum_networks: EthereumNetworks, node_id: NodeId, version_switching_mode: SubgraphVersionSwitchingMode, assignment_event_stream_cancel_guard: CancelGuard, // cancels on drop } -impl SubgraphRegistrar +impl SubgraphRegistrar where L: LinkResolver + Clone, P: SubgraphAssignmentProviderTrait, - S: Store, - CS: ChainStore, + S: SubgraphStore, + BS: BlockStore, SM: SubscriptionManager, { pub fn new( @@ -51,7 +51,7 @@ where provider: Arc

, store: Arc, subscription_manager: Arc, - chain_stores: HashMap>, + block_store: Arc, ethereum_networks: EthereumNetworks, node_id: NodeId, version_switching_mode: SubgraphVersionSwitchingMode, @@ -72,7 +72,7 @@ where provider, store, subscription_manager, - chain_stores, + block_store, ethereum_networks, node_id, version_switching_mode, @@ -258,12 +258,12 @@ where } #[async_trait] -impl SubgraphRegistrarTrait for SubgraphRegistrar +impl SubgraphRegistrarTrait for SubgraphRegistrar where L: LinkResolver, P: SubgraphAssignmentProviderTrait, - S: Store, - CS: ChainStore, + S: SubgraphStore, + BS: BlockStore, SM: SubscriptionManager, { async fn create_subgraph( @@ -299,7 +299,7 @@ where let network_name = manifest.network_name(); - let chain_store = self.chain_stores.get(&network_name).ok_or( + let chain_store = self.block_store.chain_store(&network_name).ok_or( SubgraphRegistrarError::NetworkNotSupported(network_name.clone()), )?; @@ -497,7 +497,7 @@ fn resolve_subgraph_chain_blocks( fn create_subgraph_version( logger: &Logger, - store: Arc, + store: Arc, chain_store: Arc, ethereum_adapter: Arc, name: SubgraphName, diff --git a/core/tests/interfaces.rs b/core/tests/interfaces.rs index 4c44c135f38..b38777d92bf 100644 --- a/core/tests/interfaces.rs +++ b/core/tests/interfaces.rs @@ -29,7 +29,7 @@ fn insert_and_query( }); transact_entity_operations( - &STORE, + &STORE.subgraph_store(), subgraph_id.clone(), GENESIS_PTR.clone(), insert_ops.collect::>(), @@ -38,7 +38,10 @@ fn insert_and_query( let document = graphql_parser::parse_query(query).unwrap().into_static(); let target = QueryTarget::Deployment(subgraph_id); let query = Query::new(document, None); - Ok(execute_subgraph_query(query, target).unwrap_first()) + Ok(execute_subgraph_query(query, target) + .first() + .unwrap() + .duplicate()) } /// Extract the data from a `QueryResult`, and panic if it has errors diff --git a/docker/Dockerfile b/docker/Dockerfile index e3182601b00..7244900c4d1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -49,6 +49,8 @@ ENV ethereum "" ENV node_role "combined-node" # The name of this node ENV node_id "default" +# The ethereum network polling interval (in milliseconds) +ENV ethereum_polling_interval "" # HTTP port EXPOSE 8000 diff --git a/docker/start b/docker/start index 07f25bc3b7d..c3e681b0e70 100755 --- a/docker/start +++ b/docker/start @@ -89,6 +89,11 @@ trap save_coredumps EXIT export PGAPPNAME="${node_id-$HOSTNAME}" +# Set custom poll interval +if [ -n "$ethereum_polling_interval" ]; then + export ETHEREUM_POLLING_INTERVAL=$ethereum_polling_interval +fi + case "${node_role-combined-node}" in query-node) start_query_node diff --git a/docs/sharding.md b/docs/sharding.md index 6a88fbba487..4544cec7cb7 100644 --- a/docs/sharding.md +++ b/docs/sharding.md @@ -39,6 +39,7 @@ and the replicas will receive 50% of the traffic each. In the `vip` shard, [store.primary] connection = "postgresql://graph:${PGPASSWORD}@primary/graph" weight = 0 +pool_size = 10 [store.primary.replicas.repl1] connection = "postgresql://graph:${PGPASSWORD}@primary-repl1/graph" weight = 1 @@ -49,6 +50,7 @@ weight = 1 [store.vip] connection = "postgresql://graph:${PGPASSWORD}@${VIP_MAIN}/graph" weight = 1 +pool_size = 10 [store.vip.replicas.repl1] connection = "postgresql://graph:${PGPASSWORD}@${VIP_REPL1}/graph" weight = 1 @@ -59,6 +61,77 @@ string](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTR passing the connection string to Postgres, environment variables embedded in the string are expanded. +### Setting the `pool_size` + +Each shard must indicate how many database connections each `graph-node` +instance should keep in its connection pool for that database. For +replicas, the pool size defaults to the pool size of the main database, but +can also be set explicitly. Such a setting replaces the setting from the +main database. + +The `pool_size` can either be a number like in the example above, in which +case any `graph-node` instance will use a connection pool of that size, or a set +of rules that uses different sizes for different `graph-node` instances, +keyed off the `node_id` set on the command line. When using rules, the +`pool_size` is set like this: + +```toml +pool_size = [ + { node = "index_node_general_.*", size = 20 }, + { node = "index_node_special_.*", size = 30 }, + { node = "query_node_.*", size = 80 } +] +``` + +Each rule consists of a regular expression `node` and the size that should +be used if the current instance's `node_id` matches that regular +expression. You can use the command `graphman config pools` to check how +many connections each `graph-node` instance will use, and how many database +connections will be opened by all `graph-node` instance. The rules are +checked in the order in which they are written, and the first one that +matches is used. It is an error if no rule matches. + +It is highly recommended to run `graphman config pools $all_nodes` every +time the configuration is changed to make sure that the connection pools +are what is expected. Here, `$all_nodes` should be a list of all the node +names that will use this configuration file. + +## Configuring Ethereum Providers + +The `[chains]` section controls the ethereum providers that `graph-node` +connects to, and where blocks and other metadata for each chain are +stored. The section consists of the name of the node doing block ingestion +(currently not used), and a list of chains. The configuration for a chain +`name` is specified in the section `[chains.]`, and consists of the +`shard` where chain data is stored and a list of providers for that +chain. For each provider, the following information must be given: + +* `label`: a label that is used when logging information about that + provider (not implemented yet) +* `transport`: one of `rpc`, `ws`, and `ipc`. Defaults to `rpc` +* `url`: the URL for the provider +* `features`: an array of features that the provider supports, either empty + or any combination of `traces` and `archive` + +The following example configures two chains, `mainnet` and `kovan`, where +blocks for `mainnet` are stored in the `vip` shard and blocks for `kovan` +are stored in the primary shard. The `mainnet` chain can use two different +providers, whereas `kovan` only has one provider. + +```toml +[chains] +ingestor = "block_ingestor_node" +[chains.mainnet] +shard = "vip" +provider = [ + { label = "mainnet1", url = "http://..", features = [] }, + { label = "mainnet2", url = "http://..", features = [ "archive", "traces" ] } +] +[chains.kovan] +shard = "primary" +provider = [ { label = "kovan", url = "http://..", features = [] } ] +``` + ## Controlling Deployment When `graph-node` receives a request to deploy a new subgraph deployment, @@ -108,6 +181,19 @@ indexers = [ ``` +## Query nodes + +Nodes can be configured to explicitly be query nodes by including the +following in the configuration file: +```toml +[general] +query = "" +``` + +Any node whose `--node-id` matches the regular expression will be set up to +only respond to queries. For now, that only means that the node will not +try to connect to any of the configured Ethereum providers. + ## Basic Setup The following file is equivalent to using the `--postgres-url` command line @@ -126,18 +212,17 @@ indexers = [ <.. list of all indexing nodes ..> ] A configuration file can be checked for validity by passing the `--check-config` flag to `graph-node`. The command ```shell -graph-node --config $CONFIG_FILE --check-config --ethereum-rpc 'something' +graph-node --config $CONFIG_FILE --check-config ``` will read the configuration file and print information about syntax errors or, for -valid files, a JSON representation of the configuration. (The fact that -`--ethereum-rpc` needs to be specified will be addressed in a future release) +valid files, a JSON representation of the configuration. ## Simulating deployment placement Given a configuration file, placement of newly deployed subgraphs can be simulated with ```shell -graphman --config $CONFIG_FILE place some/subgraph mainnet +graphman --config $CONFIG_FILE config place some/subgraph mainnet ``` The command will not make any changes, but simply print where that subgraph would be placed. The output will indicate the database shard that will hold diff --git a/example/Cargo.toml b/example/Cargo.toml new file mode 100644 index 00000000000..90423755b4b --- /dev/null +++ b/example/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "get_version" +description = "Implementation of an IPFS HTTP API client" +authors = ["Ferris Tseng "] +edition = "2018" +documentation = "https://docs.rs/ipfs-api" +repository = "https://github.com/ferristseng/rust-ipfs-api" +keywords = ["ipfs"] +categories = ["filesystem", "web-programming"] +version = "0.10.0" +license = "MIT OR Apache-2.0" + +[[bin]] +name = "get_version" +path = "get_version.rs" + +[features] +default = ["with-hyper-tls", "with-builder"] +with-hyper-tls = ["with-hyper", "hyper-tls"] +with-hyper-rustls = ["with-hyper", "hyper-rustls"] +with-hyper = ["hyper", "hyper-multipart-rfc7578", "failure"] +with-actix = ["actix-http", "actix-multipart-rfc7578", "awc", "derive_more"] +with-builder = ["typed-builder"] +# Old features, kept for compatibility +actix = ["with-actix"] +builder = ["with-builder"] + +[dependencies] +ipfs-api = "0.7.1" +actix-http = { version = "1.0", optional = true } +actix-multipart-rfc7578 = { version = "0.3.0-rc", optional = true } +awc = { version = "1.0", optional = true } +bytes = "0.5" +derive_more = { version = "0.99", optional = true } +failure = { version = "0.1.6", optional = true } +futures = "0.3.12" +http = "0.2" +hyper = { version = "0.13", optional = true } +hyper-tls = { version = "0.4", optional = true } +hyper-rustls = { version = "0.20", optional = true } +hyper-multipart-rfc7578 = { version = "0.4.0-rc", optional = true } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_urlencoded = "0.6" +tokio = "0.2" +tokio-util = { version = "0.3", features = ["codec"] } +walkdir = "2.3" +dirs = "3.0" +parity-multiaddr = "0.9" +typed-builder = { version = "0.6", optional = true } + +[dev-dependencies] +actix-http = "1.0" +actix-multipart-rfc7578 = "0.3.0-rc" +actix-rt = "1.0" +awc = "1.0" +hyper = "0.13" +hyper-tls = "0.4" +tar = "0.4" +tokio = { version = "0.2", features = ["rt-threaded", "macros", "time"] } diff --git a/example/get_version.rs b/example/get_version.rs new file mode 100644 index 00000000000..78175b7ecd0 --- /dev/null +++ b/example/get_version.rs @@ -0,0 +1,25 @@ +// Copyright 2017 rust-ipfs-api Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +// + +use ipfs_api::IpfsClient; + +// Creates an Ipfs client, and gets the version of the Ipfs server. +// +#[cfg_attr(feature = "with-actix", actix_rt::main)] +#[cfg_attr(feature = "with-hyper", tokio::main)] +async fn main() { + eprintln!("connecting to localhost:5001..."); + + let client = IpfsClient::default(); + + match client.version().await { + Ok(version) => eprintln!("version: {:?}", version.version), + Err(e) => eprintln!("error getting version: {}", e), + } +} + diff --git a/example/outputnot.txt b/example/outputnot.txt new file mode 100644 index 00000000000..9d5fb8f9866 --- /dev/null +++ b/example/outputnot.txt @@ -0,0 +1,153 @@ + Compiling proc-macro2 v1.0.24 + Compiling libc v0.2.80 + Compiling unicode-xid v0.2.1 + Compiling syn v1.0.48 + Compiling cfg-if v0.1.10 + Compiling autocfg v1.0.1 + Compiling typenum v1.12.0 + Compiling log v0.4.11 + Compiling slab v0.4.2 + Compiling memchr v2.3.4 + Compiling futures-core v0.3.8 + Compiling bytes v0.5.6 + Compiling lazy_static v1.4.0 + Compiling futures-sink v0.3.8 + Compiling fnv v1.0.7 + Compiling proc-macro-hack v0.5.19 + Compiling proc-macro-nested v0.1.6 + Compiling pin-project-lite v0.1.11 + Compiling itoa v0.4.6 + Compiling once_cell v1.5.2 + Compiling pkg-config v0.3.19 + Compiling version_check v0.9.2 + Compiling byteorder v1.3.4 + Compiling cc v1.0.62 + Compiling pin-project-internal v0.4.27 + Compiling pin-utils v0.1.0 + Compiling futures-io v0.3.8 + Compiling byte-tools v0.3.1 + Compiling tinyvec_macros v0.1.0 + Compiling serde_derive v1.0.117 + Compiling getrandom v0.1.15 + Compiling matches v0.1.8 + Compiling serde v1.0.117 + Compiling keccak v0.1.0 + Compiling cfg-if v1.0.0 + Compiling bitflags v1.2.1 + Compiling opaque-debug v0.2.3 + Compiling block-padding v0.2.1 + Compiling hashbrown v0.9.1 + Compiling httparse v1.3.4 + Compiling arrayref v0.3.6 + Compiling openssl v0.10.30 + Compiling foreign-types-shared v0.1.1 + Compiling percent-encoding v2.1.0 + Compiling subtle v1.0.0 + Compiling rand_core v0.4.2 + Compiling fake-simd v0.1.2 + Compiling try-lock v0.2.3 + Compiling opaque-debug v0.3.0 + Compiling native-tls v0.2.6 + Compiling ppv-lite86 v0.2.10 + Compiling tower-service v0.3.0 + Compiling cpuid-bool v0.1.2 + Compiling constant_time_eq v0.1.5 + Compiling ryu v1.0.5 + Compiling gimli v0.23.0 + Compiling openssl-probe v0.1.2 + Compiling arrayvec v0.5.2 + Compiling adler v0.2.3 + Compiling httpdate v0.3.2 + Compiling failure_derive v0.1.8 + Compiling mime v0.3.16 + Compiling static_assertions v1.1.0 + Compiling data-encoding v2.3.1 + Compiling serde_json v1.0.59 + Compiling object v0.22.0 + Compiling rustc-demangle v0.1.18 + Compiling unsigned-varint v0.3.3 + Compiling dtoa v0.4.6 + Compiling bs58 v0.3.1 + Compiling unsigned-varint v0.5.1 + Compiling same-file v1.0.6 + Compiling bs58 v0.4.0 + Compiling indexmap v1.6.0 + Compiling miniz_oxide v0.4.3 + Compiling futures-channel v0.3.8 + Compiling tracing-core v0.1.17 + Compiling http v0.2.1 + Compiling futures-task v0.3.8 + Compiling generic-array v0.14.4 + Compiling block-padding v0.1.5 + Compiling tinyvec v1.0.1 + Compiling unicode-bidi v0.3.4 + Compiling openssl-sys v0.9.58 + Compiling foreign-types v0.3.2 + Compiling form_urlencoded v1.0.0 + Compiling rand_core v0.3.1 + Compiling blake2b_simd v0.5.11 + Compiling blake2s_simd v0.5.11 + Compiling addr2line v0.14.0 + Compiling walkdir v2.3.1 + Compiling unicode-normalization v0.1.14 + Compiling http-body v0.3.1 + Compiling tracing v0.1.21 + Compiling want v0.3.0 + Compiling iovec v0.1.4 + Compiling net2 v0.2.35 + Compiling num_cpus v1.13.0 + Compiling signal-hook-registry v1.3.0 + Compiling socket2 v0.3.16 + Compiling dirs-sys v0.3.5 + Compiling rand v0.5.6 + Compiling quote v1.0.7 + Compiling generic-array v0.12.3 + Compiling idna v0.2.0 + Compiling mio v0.6.22 + Compiling rand_core v0.5.1 + Compiling dirs v2.0.2 + Compiling dirs v3.0.1 + Compiling backtrace v0.3.54 + Compiling digest v0.8.1 + Compiling block-buffer v0.7.3 + Compiling crypto-mac v0.7.0 + Compiling block-buffer v0.9.0 + Compiling digest v0.9.0 + Compiling url v2.2.0 + Compiling rand_chacha v0.2.2 + Compiling mio-uds v0.6.8 + Compiling sha3 v0.8.2 + Compiling sha-1 v0.8.2 + Compiling sha2 v0.8.2 + Compiling blake2 v0.8.1 + Compiling sha3 v0.9.1 + Compiling sha-1 v0.9.2 + Compiling sha2 v0.9.2 + Compiling rand v0.7.3 + Compiling synstructure v0.12.4 + Compiling multihash v0.11.4 + Compiling parity-multihash v0.2.3 + Compiling pin-project-internal v1.0.1 + Compiling futures-macro v0.3.8 + Compiling tokio-macros v0.2.6 + Compiling typed-builder v0.6.0 + Compiling tokio v0.2.23 + Compiling pin-project v1.0.1 + Compiling failure v0.1.8 + Compiling pin-project v0.4.27 + Compiling futures-util v0.3.8 + Compiling tracing-futures v0.2.4 + Compiling tokio-util v0.3.1 + Compiling tokio-tls v0.3.1 + Compiling tokio-util v0.2.0 + Compiling futures-executor v0.3.8 + Compiling h2 v0.2.7 + Compiling futures v0.3.8 + Compiling common-multipart-rfc7578 v0.2.0-rc + Compiling serde_urlencoded v0.6.1 + Compiling parity-multiaddr v0.7.3 + Compiling hyper v0.13.9 + Compiling hyper-tls v0.4.3 + Compiling hyper-multipart-rfc7578 v0.4.0-rc + Compiling ipfs-api v0.7.1 + Compiling get_version v0.10.0 diff --git a/example/outputok.txt b/example/outputok.txt new file mode 100644 index 00000000000..1e0e974237b --- /dev/null +++ b/example/outputok.txt @@ -0,0 +1,156 @@ + Compiling libc v0.2.83 + Compiling proc-macro2 v1.0.24 + Compiling unicode-xid v0.2.1 + Compiling syn v1.0.60 + Compiling cfg-if v1.0.0 + Compiling autocfg v1.0.1 + Compiling typenum v1.12.0 + Compiling log v0.4.14 + Compiling slab v0.4.2 + Compiling memchr v2.3.4 + Compiling futures-core v0.3.12 + Compiling lazy_static v1.4.0 + Compiling bytes v0.5.6 + Compiling cfg-if v0.1.10 + Compiling proc-macro-hack v0.5.19 + Compiling futures-sink v0.3.12 + Compiling fnv v1.0.7 + Compiling pin-project-lite v0.2.4 + Compiling proc-macro-nested v0.1.7 + Compiling once_cell v1.5.2 + Compiling cc v1.0.66 + Compiling pkg-config v0.3.19 + Compiling itoa v0.4.7 + Compiling version_check v0.9.2 + Compiling pin-project-lite v0.1.11 + Compiling pin-utils v0.1.0 + Compiling futures-io v0.3.12 + Compiling pin-project-internal v0.4.27 + Compiling byte-tools v0.3.1 + Compiling matches v0.1.8 + Compiling getrandom v0.1.16 + Compiling bytes v1.0.1 + Compiling byteorder v1.4.2 + Compiling tinyvec_macros v0.1.0 + Compiling serde_derive v1.0.123 + Compiling opaque-debug v0.2.3 + Compiling keccak v0.1.0 + Compiling bitflags v1.2.1 + Compiling serde v1.0.123 + Compiling httparse v1.3.4 + Compiling hashbrown v0.9.1 + Compiling foreign-types-shared v0.1.1 + Compiling block-padding v0.2.1 + Compiling percent-encoding v2.1.0 + Compiling openssl v0.10.32 + Compiling arrayref v0.3.6 + Compiling subtle v1.0.0 + Compiling rand_core v0.4.2 + Compiling ppv-lite86 v0.2.10 + Compiling fake-simd v0.1.2 + Compiling opaque-debug v0.3.0 + Compiling native-tls v0.2.7 + Compiling try-lock v0.2.3 + Compiling arrayvec v0.5.2 + Compiling gimli v0.23.0 + Compiling httpdate v0.3.2 + Compiling tower-service v0.3.1 + Compiling cpuid-bool v0.1.2 + Compiling constant_time_eq v0.1.5 + Compiling failure_derive v0.1.8 + Compiling ryu v1.0.5 + Compiling adler v0.2.3 + Compiling openssl-probe v0.1.2 + Compiling rustc-demangle v0.1.18 + Compiling serde_json v1.0.61 + Compiling unsigned-varint v0.3.3 + Compiling static_assertions v1.1.0 + Compiling data-encoding v2.3.1 + Compiling mime v0.3.16 + Compiling object v0.23.0 + Compiling unsigned-varint v0.5.1 + Compiling bs58 v0.3.1 + Compiling same-file v1.0.6 + Compiling dtoa v0.4.7 + Compiling bs58 v0.4.0 + Compiling indexmap v1.6.1 + Compiling miniz_oxide v0.4.3 + Compiling tracing-core v0.1.17 + Compiling futures-channel v0.3.12 + Compiling futures-task v0.3.12 + Compiling generic-array v0.14.4 + Compiling block-padding v0.1.5 + Compiling unicode-bidi v0.3.4 + Compiling tinyvec v1.1.1 + Compiling http v0.2.3 + Compiling openssl-sys v0.9.60 + Compiling foreign-types v0.3.2 + Compiling form_urlencoded v1.0.0 + Compiling rand_core v0.3.1 + Compiling blake2s_simd v0.5.11 + Compiling blake2b_simd v0.5.11 + Compiling addr2line v0.14.1 + Compiling walkdir v2.3.1 + Compiling unicode-normalization v0.1.16 + Compiling http-body v0.3.1 + Compiling tracing v0.1.22 + Compiling want v0.3.0 + Compiling quote v1.0.8 + Compiling iovec v0.1.4 + Compiling net2 v0.2.37 + Compiling signal-hook-registry v1.3.0 + Compiling num_cpus v1.13.0 + Compiling socket2 v0.3.19 + Compiling rand v0.5.6 + Compiling dirs-sys v0.3.5 + Compiling generic-array v0.12.3 + Compiling idna v0.2.0 + Compiling mio v0.6.23 + Compiling rand_core v0.5.1 + Compiling dirs v2.0.2 + Compiling dirs v3.0.1 + Compiling backtrace v0.3.56 + Compiling digest v0.8.1 + Compiling block-buffer v0.7.3 + Compiling crypto-mac v0.7.0 + Compiling block-buffer v0.9.0 + Compiling digest v0.9.0 + Compiling url v2.2.0 + Compiling rand_chacha v0.2.2 + Compiling mio-uds v0.6.8 + Compiling sha3 v0.8.2 + Compiling sha-1 v0.8.2 + Compiling sha2 v0.8.2 + Compiling blake2 v0.8.1 + Compiling sha3 v0.9.1 + Compiling sha-1 v0.9.2 + Compiling sha2 v0.9.2 + Compiling rand v0.7.3 + Compiling synstructure v0.12.4 + Compiling multihash v0.11.4 + Compiling parity-multihash v0.2.3 + Compiling pin-project-internal v1.0.4 + Compiling futures-macro v0.3.12 + Compiling tokio-macros v0.2.6 + Compiling typed-builder v0.6.0 + Compiling tokio v0.2.24 + Compiling failure v0.1.8 + Compiling pin-project v0.4.27 + Compiling pin-project v1.0.4 + Compiling futures-util v0.3.12 + Compiling tracing-futures v0.2.4 + Compiling tokio-util v0.3.1 + Compiling tokio-tls v0.3.1 + Compiling tokio-util v0.2.0 + Compiling futures-executor v0.3.12 + Compiling h2 v0.2.7 + Compiling futures v0.3.12 + Compiling common-multipart-rfc7578 v0.2.0-rc + Compiling serde_urlencoded v0.6.1 + Compiling parity-multiaddr v0.7.3 + Compiling parity-multiaddr v0.9.7 + Compiling hyper v0.13.9 + Compiling hyper-multipart-rfc7578 v0.4.0-rc + Compiling hyper-tls v0.4.3 + Compiling ipfs-api v0.7.2 + Compiling get_version v0.10.0 diff --git a/graph/Cargo.toml b/graph/Cargo.toml index e3b227d7967..b14d5923d8a 100644 --- a/graph/Cargo.toml +++ b/graph/Cargo.toml @@ -23,7 +23,7 @@ reqwest = "0.10" # ethabi, but long term we want to find a way to drop our fork. ethabi = { git = "https://github.com/graphprotocol/ethabi.git", branch = "master" } hex = "0.4.2" -http = "0.2" +http = "0.2.3" futures = "0.1.21" graphql-parser = "0.3" lazy_static = "1.4.0" @@ -33,9 +33,9 @@ num_cpus = "1.13.0" num-traits = "0.2" rand = "0.6.1" semver = "0.10.0" -serde = { version = "1.0", features = ["rc"] } -serde_derive = "1.0" -serde_json = { version = "1.0", features = ["arbitrary_precision"] } +serde = { version = "1.0.123", features = ["rc"] } +serde_derive = "1.0.123" +serde_json = { version = "1.0.61", features = ["arbitrary_precision"] } serde_yaml = "0.8" slog = { version = "2.5.2", features = ["release_max_level_trace", "max_level_trace"] } stable-hash = { git = "https://github.com/graphprotocol/stable-hash" } @@ -46,10 +46,10 @@ slog-envlogger = "2.1.0" slog-term = "2.6.0" petgraph = "0.5.1" tiny-keccak = "1.5.0" -tokio = { version = "0.2.22", features = ["stream", "rt-threaded", "rt-util", "blocking", "time", "sync", "macros", "test-util"] } +tokio = { version = "0.2.24", features = ["stream", "rt-threaded", "rt-util", "blocking", "time", "sync", "macros", "test-util"] } tokio-retry = { git = "https://github.com/graphprotocol/rust-tokio-retry", branch = "update-to-tokio-02" } -url = "2.1.1" -prometheus = "0.7.0" +url = "2.2.0" +prometheus = "0.11.0" priority-queue = "0.7.0" futures03 = { version = "0.3.1", package = "futures", features = ["compat"] } uuid = { version = "0.8.1", features = ["v4"] } diff --git a/graph/src/components/ethereum/adapter.rs b/graph/src/components/ethereum/adapter.rs index 45a346fffaf..e498a3e72da 100644 --- a/graph/src/components/ethereum/adapter.rs +++ b/graph/src/components/ethereum/adapter.rs @@ -19,6 +19,7 @@ use crate::prelude::*; pub type EventSignature = H256; +#[derive(Clone)] /// A collection of attributes that (kind of) uniquely identify an Ethereum blockchain. pub struct EthereumNetworkIdentifier { pub net_version: String, diff --git a/graph/src/components/ethereum/types.rs b/graph/src/components/ethereum/types.rs index 2a4b46659a0..491d7c8fb06 100644 --- a/graph/src/components/ethereum/types.rs +++ b/graph/src/components/ethereum/types.rs @@ -528,6 +528,21 @@ impl TryFrom<(&str, i64)> for EthereumBlockPointer { } } +impl TryFrom<(&[u8], i64)> for EthereumBlockPointer { + type Error = anyhow::Error; + + fn try_from((bytes, number): (&[u8], i64)) -> Result { + let hash = if bytes.len() == H256::len_bytes() { + H256::from_slice(bytes) + } else { + return Err(anyhow!( + "invalid H256 value `{}` has {} bytes instead of {}" + )); + }; + Ok(EthereumBlockPointer::from((hash, number))) + } +} + impl<'a> From<&'a EthereumCall> for EthereumBlockPointer { fn from(call: &'a EthereumCall) -> EthereumBlockPointer { EthereumBlockPointer { diff --git a/graph/src/components/server/index_node.rs b/graph/src/components/server/index_node.rs index 3f59380f698..0087f3cc590 100644 --- a/graph/src/components/server/index_node.rs +++ b/graph/src/components/server/index_node.rs @@ -16,7 +16,7 @@ pub struct VersionInfo { pub description: Option, pub repository: Option, pub schema: Arc, - pub network: Option, + pub network: String, } /// Common trait for index node server implementations. diff --git a/graph/src/components/store.rs b/graph/src/components/store.rs index 773b54dff1e..d679350c444 100644 --- a/graph/src/components/store.rs +++ b/graph/src/components/store.rs @@ -901,6 +901,12 @@ impl From<::diesel::result::Error> for StoreError { } } +impl From<::diesel::r2d2::PoolError> for StoreError { + fn from(e: ::diesel::r2d2::PoolError) -> Self { + StoreError::Unknown(e.into()) + } +} + impl From for StoreError { fn from(e: Error) -> Self { StoreError::Unknown(e) @@ -935,7 +941,7 @@ pub trait SubscriptionManager: Send + Sync + 'static { /// Common trait for store implementations. #[async_trait] -pub trait Store: Send + Sync + 'static { +pub trait SubgraphStore: Send + Sync + 'static { /// Get a pointer to the most recently processed block in the subgraph. fn block_ptr( &self, @@ -1005,12 +1011,14 @@ pub trait Store: Send + Sync + 'static { /// Find the deployment for the current version of subgraph `name` and /// return details about it needed for executing queries - fn deployment_state_from_name(&self, name: SubgraphName) - -> Result; + async fn deployment_state_from_name( + &self, + name: SubgraphName, + ) -> Result; /// Find the deployment for the subgraph deployment `id` and /// return details about it needed for executing queries - fn deployment_state_from_id( + async fn deployment_state_from_id( &self, id: SubgraphDeploymentId, ) -> Result; @@ -1072,17 +1080,15 @@ pub trait Store: Send + Sync + 'static { fn unassign_subgraph(&self, id: &SubgraphDeploymentId) -> Result<(), StoreError>; - /// Start an existing subgraph deployment. This will reset the state of - /// the subgraph to a known good state. `ops` needs to contain all the - /// operations on the subgraph of subgraphs to reset the metadata of the - /// subgraph + /// Start an existing subgraph deployment. fn start_subgraph_deployment( &self, logger: &Logger, subgraph_id: &SubgraphDeploymentId, ) -> Result<(), StoreError>; - fn status(&self, filter: status::Filter) -> Result, StoreError>; + /// Remove the fatal error from a subgraph and check if it is healthy or unhealthy. + fn unfail(&self, subgraph_id: &SubgraphDeploymentId) -> Result<(), StoreError>; /// Load the dynamic data sources for the given deployment async fn load_dynamic_data_sources( @@ -1110,24 +1116,10 @@ pub trait Store: Send + Sync + 'static { /// Return the name of the network that the subgraph is indexing from. The /// names returned are things like `mainnet` or `ropsten` - fn network_name( - &self, - subgraph_id: &SubgraphDeploymentId, - ) -> Result, StoreError>; - - /// Support for the explorer-specific API - fn version_info(&self, version_id: &str) -> Result; - - /// Support for the explorer-specific API; note that `subgraph_id` must be - /// the id of an entry in `subgraphs.subgraph`, not that of a deployment. - /// The return values are the ids of the `subgraphs.subgraph_version` for - /// the current and pending versions of the subgraph - fn versions_for_subgraph_id( - &self, - subgraph_id: &str, - ) -> Result<(Option, Option), StoreError>; + fn network_name(&self, subgraph_id: &SubgraphDeploymentId) -> Result; } +#[async_trait] pub trait QueryStoreManager: Send + Sync + 'static { /// Get a new `QueryStore`. A `QueryStore` is tied to a DB replica, so if Graph Node is /// configured to use secondary DB servers the queries will be distributed between servers. @@ -1138,7 +1130,7 @@ pub trait QueryStoreManager: Send + Sync + 'static { /// metadata about the deployment `id` (but not metadata about other deployments). /// /// If `for_subscription` is true, the main replica will always be used. - fn query_store( + async fn query_store( &self, target: QueryTarget, for_subscription: bool, @@ -1161,7 +1153,7 @@ pub type PoolWaitStats = Arc>; // The store trait must be implemented manually because mockall does not support async_trait, nor borrowing from arguments. #[async_trait] -impl Store for MockStore { +impl SubgraphStore for MockStore { fn block_ptr( &self, _subgraph_id: &SubgraphDeploymentId, @@ -1228,11 +1220,14 @@ impl Store for MockStore { unimplemented!() } - fn deployment_state_from_name(&self, _: SubgraphName) -> Result { + async fn deployment_state_from_name( + &self, + _: SubgraphName, + ) -> Result { unimplemented!() } - fn deployment_state_from_id( + async fn deployment_state_from_id( &self, _: SubgraphDeploymentId, ) -> Result { @@ -1283,15 +1278,15 @@ impl Store for MockStore { unimplemented!() } - fn is_deployment_synced(&self, _: &SubgraphDeploymentId) -> Result { + fn unfail(&self, _: &SubgraphDeploymentId) -> Result<(), StoreError> { unimplemented!() } - fn deployment_synced(&self, _: &SubgraphDeploymentId) -> Result<(), Error> { + fn is_deployment_synced(&self, _: &SubgraphDeploymentId) -> Result { unimplemented!() } - fn status(&self, _: status::Filter) -> Result, StoreError> { + fn deployment_synced(&self, _: &SubgraphDeploymentId) -> Result<(), Error> { unimplemented!() } @@ -1322,20 +1317,21 @@ impl Store for MockStore { unimplemented!() } - fn network_name(&self, _: &SubgraphDeploymentId) -> Result, StoreError> { + fn network_name(&self, _: &SubgraphDeploymentId) -> Result { unimplemented!() } +} - fn version_info(&self, _: &str) -> Result { - unimplemented!() - } +pub trait BlockStore: Send + Sync + 'static { + type ChainStore: ChainStore; - fn versions_for_subgraph_id( - &self, - _: &str, - ) -> Result<(Option, Option), StoreError> { - unimplemented!() - } + fn chain_store(&self, network: &str) -> Option>; +} + +pub trait CallCache: Send + Sync + 'static { + type EthereumCallCache: EthereumCallCache; + + fn ethereum_call_cache(&self, network: &str) -> Option>; } /// Common trait for blockchain store implementations. @@ -1466,11 +1462,46 @@ pub trait QueryStore: Send + Sync { /// Find the current state for the subgraph deployment `id` and /// return details about it needed for executing queries - fn deployment_state(&self) -> Result; + async fn deployment_state(&self) -> Result; fn api_schema(&self) -> Result, QueryExecutionError>; - fn network_name(&self) -> Result, QueryExecutionError>; + fn network_name(&self) -> &str; +} + +/// A view of the store that can provide information about the indexing status +/// of any subgraph and any deployment +pub trait StatusStore: Send + Sync + 'static { + fn status(&self, filter: status::Filter) -> Result, StoreError>; + + /// Support for the explorer-specific API + fn version_info(&self, version_id: &str) -> Result; + + /// Support for the explorer-specific API; note that `subgraph_id` must be + /// the id of an entry in `subgraphs.subgraph`, not that of a deployment. + /// The return values are the ids of the `subgraphs.subgraph_version` for + /// the current and pending versions of the subgraph + fn versions_for_subgraph_id( + &self, + subgraph_id: &str, + ) -> Result<(Option, Option), StoreError>; + + fn supports_proof_of_indexing<'a>( + self: Arc, + subgraph_id: &'a SubgraphDeploymentId, + ) -> DynTryFuture<'a, bool>; + + /// A value of None indicates that the table is not available. Re-deploying + /// the subgraph fixes this. It is undesirable to force everything to + /// re-sync from scratch, so existing deployments will continue without a + /// Proof of Indexing. Once all subgraphs have been re-deployed the Option + /// can be removed. + fn get_proof_of_indexing<'a>( + self: Arc, + subgraph_id: &'a SubgraphDeploymentId, + indexer: &'a Option

, + block: EthereumBlockPointer, + ) -> DynTryFuture<'a, Option<[u8; 32]>>; } /// An entity operation that can be transacted into the store; as opposed to @@ -1565,7 +1596,7 @@ pub struct EntityCache { in_handler: bool, /// The store is only used to read entities. - pub store: Arc, + pub store: Arc, } impl Debug for EntityCache { @@ -1583,7 +1614,7 @@ pub struct ModificationsAndCache { } impl EntityCache { - pub fn new(store: Arc) -> Self { + pub fn new(store: Arc) -> Self { Self { current: LfuCache::new(), updates: HashMap::new(), @@ -1594,7 +1625,7 @@ impl EntityCache { } pub fn with_current( - store: Arc, + store: Arc, current: LfuCache>, ) -> EntityCache { EntityCache { @@ -1695,7 +1726,7 @@ impl EntityCache { /// Also returns the updated `LfuCache`. pub fn as_modifications( mut self, - store: &(impl Store + ?Sized), + store: &(impl SubgraphStore + ?Sized), ) -> Result { assert!(!self.in_handler); @@ -1786,7 +1817,7 @@ impl LfuCache> { // Helper for cached lookup of an entity. fn get_entity( &mut self, - store: &(impl Store + ?Sized), + store: &(impl SubgraphStore + ?Sized), key: &EntityKey, ) -> Result, QueryExecutionError> { match self.get(&key) { diff --git a/graph/src/components/subgraph/instance.rs b/graph/src/components/subgraph/instance.rs index 62d13aa2736..f089d98f2b9 100644 --- a/graph/src/components/subgraph/instance.rs +++ b/graph/src/components/subgraph/instance.rs @@ -31,7 +31,10 @@ pub struct BlockState { } impl BlockState { - pub fn new(store: Arc, lfu_cache: LfuCache>) -> Self { + pub fn new( + store: Arc, + lfu_cache: LfuCache>, + ) -> Self { BlockState { entity_cache: EntityCache::with_current(store, lfu_cache), deterministic_errors: Vec::new(), diff --git a/graph/src/data/query/result.rs b/graph/src/data/query/result.rs index 048f51c020a..c4e8ab0afd7 100644 --- a/graph/src/data/query/result.rs +++ b/graph/src/data/query/result.rs @@ -40,6 +40,7 @@ where pub type Data = BTreeMap; +#[derive(Debug)] /// A collection of query results that is serialized as a single result. pub struct QueryResults { results: Vec>, @@ -151,10 +152,6 @@ impl QueryResults { self.results.push(other); } - pub fn unwrap_first(self) -> QueryResult { - Arc::try_unwrap(self.results.into_iter().next().unwrap()).unwrap() - } - pub fn as_http_response>(&self) -> http::Response { let status_code = http::StatusCode::OK; let json = @@ -193,6 +190,18 @@ impl QueryResult { } } + /// This is really `clone`, but we do not want to implement `Clone`; + /// this is only meant for test purposes and should not be used in production + /// code since cloning query results can be very expensive + #[cfg(debug_assertions)] + pub fn duplicate(&self) -> Self { + Self { + data: self.data.clone(), + errors: self.errors.clone(), + deployment: self.deployment.clone(), + } + } + pub fn has_errors(&self) -> bool { return !self.errors.is_empty(); } diff --git a/graph/src/data/schema.rs b/graph/src/data/schema.rs index 24505c7f2b1..f9717b250aa 100644 --- a/graph/src/data/schema.rs +++ b/graph/src/data/schema.rs @@ -1,4 +1,4 @@ -use crate::components::store::Store; +use crate::components::store::SubgraphStore; use crate::data::graphql::ext::{DirectiveExt, DirectiveFinder, DocumentExt, TypeExt, ValueExt}; use crate::data::store::ValueType; use crate::data::subgraph::{SubgraphDeploymentId, SubgraphName}; @@ -325,7 +325,10 @@ impl SchemaReference { SchemaReference { subgraph } } - pub fn resolve(&self, store: Arc) -> Result, SchemaImportError> { + pub fn resolve( + &self, + store: Arc, + ) -> Result, SchemaImportError> { store .input_schema(&self.subgraph) .map_err(|_| SchemaImportError::ImportedSchemaNotFound(self.clone())) @@ -423,7 +426,7 @@ impl Schema { } } - pub fn resolve_schema_references( + pub fn resolve_schema_references( &self, store: Arc, ) -> ( @@ -436,7 +439,7 @@ impl Schema { (schemas, import_errors) } - fn resolve_import_graph( + fn resolve_import_graph( &self, store: Arc, schemas: &mut HashMap>, diff --git a/graph/src/data/store/scalar.rs b/graph/src/data/store/scalar.rs index a5d9d356ec3..cdca01827b2 100644 --- a/graph/src/data/store/scalar.rs +++ b/graph/src/data/store/scalar.rs @@ -14,7 +14,7 @@ use stable_hash::{ use std::convert::{TryFrom, TryInto}; use std::fmt::{self, Display, Formatter}; use std::io::Write; -use std::ops::{Add, Deref, Div, Mul, Rem, Sub}; +use std::ops::{Add, BitAnd, BitOr, Deref, Div, Mul, Rem, Shl, Shr, Sub}; use std::str::FromStr; pub use num_bigint::Sign as BigIntSign; @@ -446,6 +446,38 @@ impl Rem for BigInt { } } +impl BitOr for BigInt { + type Output = Self; + + fn bitor(self, other: Self) -> Self { + Self::from(self.0.bitor(other.0)) + } +} + +impl BitAnd for BigInt { + type Output = Self; + + fn bitand(self, other: Self) -> Self { + Self::from(self.0.bitand(other.0)) + } +} + +impl Shl for BigInt { + type Output = Self; + + fn shl(self, bits: u8) -> Self { + Self::from(self.0.shl(bits.into())) + } +} + +impl Shr for BigInt { + type Output = Self; + + fn shr(self, bits: u8) -> Self { + Self::from(self.0.shr(bits.into())) + } +} + /// A byte array that's serialized as a hex string prefixed by `0x`. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Bytes(Box<[u8]>); diff --git a/graph/src/data/subgraph/mod.rs b/graph/src/data/subgraph/mod.rs index e821eea7308..b749ab7a7b3 100644 --- a/graph/src/data/subgraph/mod.rs +++ b/graph/src/data/subgraph/mod.rs @@ -17,7 +17,8 @@ use thiserror::Error; use wasmparser; use web3::types::{Address, H256}; -use crate::components::store::{Store, StoreError}; +use crate::components::link_resolver::LinkResolver; +use crate::components::store::{StoreError, SubgraphStore}; use crate::components::subgraph::DataSourceTemplateInfo; use crate::data::graphql::TryFromValue; use crate::data::query::QueryExecutionError; @@ -29,7 +30,7 @@ use crate::data::subgraph::schema::{ EthereumContractEventHandlerEntity, EthereumContractMappingEntity, EthereumContractSourceEntity, }; -use crate::{components::link_resolver::LinkResolver, prelude::CheapClone}; +use crate::prelude::CheapClone; use crate::prelude::{impl_slog_value, q, BlockNumber, Deserialize, Serialize}; use crate::util::ethereum::string_to_h256; @@ -887,7 +888,7 @@ pub struct Graft { } impl Graft { - fn validate(&self, store: Arc) -> Vec { + fn validate(&self, store: Arc) -> Vec { fn gbi(msg: String) -> Vec { vec![SubgraphManifestValidationError::GraftBaseInvalid(msg)] } @@ -960,7 +961,7 @@ impl UnvalidatedSubgraphManifest { )) } - pub fn validate( + pub fn validate( self, store: Arc, ) -> Result< diff --git a/graph/src/data/subgraph/schema.rs b/graph/src/data/subgraph/schema.rs index c9cae80f8f8..58abf8bacdf 100644 --- a/graph/src/data/subgraph/schema.rs +++ b/graph/src/data/subgraph/schema.rs @@ -1157,7 +1157,7 @@ pub struct SubgraphError { pub block_ptr: Option, pub handler: Option, - // `true` if we are certain the error is determinsitic. If in doubt, this is `false`. + // `true` if we are certain the error is deterministic. If in doubt, this is `false`. pub deterministic: bool, } diff --git a/graph/src/lib.rs b/graph/src/lib.rs index 4b5891f68f6..139041dc12c 100644 --- a/graph/src/lib.rs +++ b/graph/src/lib.rs @@ -29,6 +29,7 @@ pub use task_spawn::{ }; pub use bytes; +pub use prometheus; pub use stable_hash; pub use url; @@ -103,8 +104,8 @@ pub mod prelude { EntityChangeOperation, EntityCollection, EntityFilter, EntityKey, EntityLink, EntityModification, EntityOperation, EntityOrder, EntityQuery, EntityRange, EntityWindow, EthereumCallCache, MetadataOperation, ParentLink, PoolWaitStats, QueryStore, - QueryStoreManager, Store, StoreError, StoreEvent, StoreEventStream, StoreEventStreamBox, - WindowAttribute, BLOCK_NUMBER_MAX, SUBSCRIPTION_THROTTLE_INTERVAL, + QueryStoreManager, StoreError, StoreEvent, StoreEventStream, StoreEventStreamBox, + SubgraphStore, WindowAttribute, BLOCK_NUMBER_MAX, SUBSCRIPTION_THROTTLE_INTERVAL, }; pub use crate::components::subgraph::{ BlockState, DataSourceLoader, DataSourceTemplateInfo, HostMetrics, RuntimeHost, diff --git a/graph/tests/manifest.rs b/graph/tests/manifest.rs index d746840c2fe..205536354be 100644 --- a/graph/tests/manifest.rs +++ b/graph/tests/manifest.rs @@ -134,7 +134,7 @@ graft: specVersion: 0.0.2 "; - let store = test_store::STORE.clone(); + let store = test_store::STORE.subgraph_store(); test_store::STORE_RUNTIME.lock().unwrap().block_on(async { let unvalidated = resolve_unvalidated(YAML).await; diff --git a/graphql/Cargo.toml b/graphql/Cargo.toml index 9b18cb09bf2..4973e07daaf 100644 --- a/graphql/Cargo.toml +++ b/graphql/Cargo.toml @@ -5,15 +5,15 @@ edition = "2018" [dependencies] crossbeam = "0.8" -futures01 = { package="futures", version="0.1.29" } +futures01 = { package="futures", version="0.3.12" } graph = { path = "../graph" } graphql-parser = "0.3" -indexmap = "1.6" +indexmap = "1.6.1" Inflector = "0.11.3" -lazy_static = "1.2.0" +lazy_static = "1.4.0" uuid = { version = "0.8.1", features = ["v4"] } stable-hash = { git = "https://github.com/graphprotocol/stable-hash" } -once_cell = "1.4.1" +once_cell = "1.5.2" defer = "0.1" parking_lot = "0.11" anyhow = "1.0" diff --git a/graphql/src/execution/cache.rs b/graphql/src/execution/cache.rs index 654e20566ae..0f5fa30931f 100644 --- a/graphql/src/execution/cache.rs +++ b/graphql/src/execution/cache.rs @@ -90,6 +90,7 @@ struct CacheByBlock { // The value is `(result, n_hits)`. cache: HashMap, AtomicU64)>, + total_insert_time: Duration, } impl CacheByBlock { @@ -99,6 +100,7 @@ impl CacheByBlock { max_weight, weight: 0, cache: HashMap::new(), + total_insert_time: Duration::default(), } } @@ -114,8 +116,10 @@ impl CacheByBlock { assert!(!value.has_errors()); let fits_in_cache = self.weight + weight <= self.max_weight; if fits_in_cache { + let start = Instant::now(); self.weight += weight; self.cache.insert(key, (value, AtomicU64::new(0))); + self.total_insert_time += start.elapsed(); } fits_in_cache } @@ -129,7 +133,6 @@ pub struct QueryBlockCache { cache_by_network: Vec<(String, VecDeque)>, max_weight: usize, max_blocks: usize, - total_insert_time: Duration, } impl QueryBlockCache { @@ -139,7 +142,6 @@ impl QueryBlockCache { cache_by_network: Vec::new(), max_weight, max_blocks, - total_insert_time: Duration::default(), } } @@ -174,10 +176,7 @@ impl QueryBlockCache { // If there is already a cache by the block of this query, just add it there. if let Some(cache_by_block) = cache.iter_mut().find(|c| c.block == block_ptr) { - let start = Instant::now(); - let inserted = cache_by_block.insert(key, result.cheap_clone(), weight); - self.total_insert_time += start.elapsed(); - return inserted; + return cache_by_block.insert(key, result.cheap_clone(), weight); } // We're creating a new `CacheByBlock` if: @@ -195,8 +194,10 @@ impl QueryBlockCache { // Stats are reported in a task since we don't need the lock for it. let block = cache.pop_back().unwrap(); let shard = self.shard; - let insert_time_ms = self.total_insert_time.as_millis(); + let network = network.to_string(); + graph::spawn(async move { + let insert_time_ms = block.total_insert_time.as_millis(); let mut dead_inserts = 0; let mut total_hits = 0; for (_, hits) in block.cache.values() { @@ -209,6 +210,7 @@ impl QueryBlockCache { let n_entries = block.cache.len(); debug!(logger, "Rotating query cache, stats for last block"; "shard" => shard, + "network" => network, "entries" => n_entries, "avg_hits" => format!("{0:.2}", (total_hits as f64) / (n_entries as f64)), "dead_inserts" => dead_inserts, @@ -220,7 +222,7 @@ impl QueryBlockCache { // Create a new cache by block, insert this entry, and add it to the QUERY_CACHE. let mut cache_by_block = CacheByBlock::new(block_ptr, self.max_weight); - let cache_insert = cache_by_block.insert(key, result.cheap_clone(), weight); + let cache_insert = cache_by_block.insert(key, result, weight); cache.push_front(cache_by_block); cache_insert } diff --git a/graphql/src/runner.rs b/graphql/src/runner.rs index bb4a691e436..0a9351ccde9 100644 --- a/graphql/src/runner.rs +++ b/graphql/src/runner.rs @@ -95,7 +95,7 @@ where /// would affect a query that looked at data as fresh as `latest_block`. /// If the subgraph did change, return the `Err` that should be sent back /// to clients to indicate that condition - fn deployment_changed( + async fn deployment_changed( &self, store: &dyn QueryStore, state: DeploymentState, @@ -104,7 +104,7 @@ where if *GRAPHQL_ALLOW_DEPLOYMENT_CHANGE { return Ok(()); } - let new_state = store.deployment_state()?; + let new_state = store.deployment_state().await?; assert!(new_state.reorg_count >= state.reorg_count); if new_state.reorg_count > state.reorg_count { // One or more reorgs happened; each reorg can't have gone back @@ -140,12 +140,9 @@ where // while the query is running. `self.store` can not be used after this // point, and everything needs to go through the `store` we are // setting up here - let store = self - .store - .query_store(target, false) - .map_err(|e| QueryExecutionError::from(e))?; - let state = store.deployment_state()?; - let network = store.network_name()?; + let store = self.store.query_store(target, false).await?; + let state = store.deployment_state().await?; + let network = Some(store.network_name().to_string()); let schema = store.api_schema()?; // Test only, see c435c25decbc4ad7bbbadf8e0ced0ff2 @@ -207,6 +204,7 @@ where query.log_execution(max_block); self.deployment_changed(store.as_ref(), state, max_block as u64) + .await .map_err(QueryResults::from) .map(|()| result) } @@ -264,14 +262,14 @@ where subscription: Subscription, target: QueryTarget, ) -> Result { - let store = self.store.query_store(target, true)?; + let store = self.store.query_store(target, true).await?; let schema = store.api_schema()?; - let network = store.network_name()?; + let network = store.network_name().to_string(); let query = crate::execution::Query::new( &self.logger, schema, - network, + Some(network.clone()), subscription.query, *GRAPHQL_MAX_COMPLEXITY, *GRAPHQL_MAX_DEPTH, diff --git a/graphql/tests/query.rs b/graphql/tests/query.rs index b7316c41f55..341097123e3 100644 --- a/graphql/tests/query.rs +++ b/graphql/tests/query.rs @@ -10,14 +10,18 @@ use std::time::{Duration, Instant}; use graph::{ data::graphql::{object, object_value}, data::subgraph::schema::SubgraphError, - data::{query::CacheStatus, query::QueryTarget, subgraph::SubgraphFeature}, + data::{ + query::CacheStatus, + query::{QueryResults, QueryTarget}, + subgraph::SubgraphFeature, + }, prelude::{ async_trait, futures03::stream::StreamExt, futures03::FutureExt, futures03::TryFutureExt, o, q, serde_json, slog, tokio, Entity, EntityKey, EntityOperation, EthereumBlockPointer, FutureExtension, GraphQlRunner as _, Logger, NodeId, Query, QueryError, QueryExecutionError, QueryLoadManager, QueryResult, QueryStoreManager, QueryVariables, - Schema, Store, SubgraphDeploymentEntity, SubgraphDeploymentId, SubgraphManifest, - SubgraphName, SubgraphVersionSwitchingMode, Subscription, SubscriptionError, Value, + Schema, SubgraphDeploymentEntity, SubgraphDeploymentId, SubgraphManifest, SubgraphName, + SubgraphStore, SubgraphVersionSwitchingMode, Subscription, SubscriptionError, Value, }, }; use graph_graphql::{prelude::*, subscription::execute_subscription}; @@ -39,8 +43,7 @@ fn setup_with_features(id: &str, features: BTreeSet) -> Subgrap let id = SubgraphDeploymentId::new(id).unwrap(); let chain = vec![&*GENESIS_BLOCK, &*BLOCK_ONE, &*BLOCK_TWO]; - block_store::remove(); - block_store::insert(chain, NETWORK_NAME); + block_store::set_chain(chain, NETWORK_NAME); test_store::remove_subgraphs(); let schema = test_schema(id.clone()); @@ -57,7 +60,7 @@ fn setup_with_features(id: &str, features: BTreeSet) -> Subgrap templates: vec![], }; - insert_test_entities(STORE.as_ref(), manifest); + insert_test_entities(STORE.subgraph_store().as_ref(), manifest); id } @@ -98,7 +101,7 @@ fn test_schema(id: SubgraphDeploymentId) -> Schema { .expect("Test schema invalid") } -fn insert_test_entities(store: &impl Store, manifest: SubgraphManifest) { +fn insert_test_entities(store: &impl SubgraphStore, manifest: SubgraphManifest) { let deployment = SubgraphDeploymentEntity::new(&manifest, false, None); let name = SubgraphName::new("test/query").unwrap(); let node_id = NodeId::new("test").unwrap(); @@ -220,7 +223,7 @@ fn insert_test_entities(store: &impl Store, manifest: SubgraphManifest) { }); transact_entity_operations( - &STORE, + &STORE.subgraph_store(), id.clone(), block_ptr, insert_ops.collect::>(), @@ -253,7 +256,21 @@ async fn execute_query_document_with_variables( runner .run_query_with_complexity(query, target, None, None, None, None, false) .await - .unwrap_first() + .first() + .unwrap() + .duplicate() +} + +async fn first_result(f: F) -> QueryResult +where + F: FnOnce() -> QueryResults + Sync + Send + 'static, +{ + graph::spawn_blocking_allow_panic(f) + .await + .unwrap() + .first() + .unwrap() + .duplicate() } struct MockQueryLoadManager(Arc); @@ -788,12 +805,10 @@ fn query_complexity() { // This query is exactly at the maximum complexity. let id2 = id.clone(); - let result = graph::spawn_blocking_allow_panic(move || { + let result = first_result(move || { execute_subgraph_query_with_complexity(query, id2.into(), max_complexity) }) - .await - .unwrap() - .unwrap_first(); + .await; assert!(!result.has_errors()); let query = Query::new( @@ -821,12 +836,11 @@ fn query_complexity() { ); // The extra introspection causes the complexity to go over. - let result = graph::spawn_blocking_allow_panic(move || { + let result = first_result(move || { execute_subgraph_query_with_complexity(query, id.into(), max_complexity) }) - .await - .unwrap(); - match result.unwrap_first().to_result().unwrap_err()[0] { + .await; + match result.to_result().unwrap_err()[0] { QueryError::ExecutionError(QueryExecutionError::TooComplex(1_010_200, _)) => (), _ => panic!("did not catch complexity"), }; @@ -837,7 +851,11 @@ fn query_complexity() { fn query_complexity_subscriptions() { run_test_sequentially(setup, |_, id| async move { let logger = Logger::root(slog::Discard, o!()); - let store = STORE.clone().query_store(id.clone().into(), true).unwrap(); + let store = STORE + .clone() + .query_store(id.clone().into(), true) + .await + .unwrap(); let query = Query::new( graphql_parser::parse_query( @@ -869,7 +887,7 @@ fn query_complexity_subscriptions() { max_skip: std::u32::MAX, load_manager: mock_query_load_manager(), }; - let schema = STORE.api_schema(&id).unwrap(); + let schema = STORE.subgraph_store().api_schema(&id).unwrap(); // This query is exactly at the maximum complexity. // FIXME: Not collecting the stream because that will hang the test. @@ -900,7 +918,11 @@ fn query_complexity_subscriptions() { None, ); - let store = STORE.clone().query_store(id.clone().into(), true).unwrap(); + let store = STORE + .clone() + .query_store(id.clone().into(), true) + .await + .unwrap(); let options = SubscriptionExecutionOptions { logger, @@ -936,12 +958,10 @@ fn instant_timeout() { None, ); - match graph::spawn_blocking_allow_panic(move || { + match first_result(move || { execute_subgraph_query_with_deadline(query, id.into(), Some(Instant::now())) }) .await - .unwrap() - .unwrap_first() .to_result() .unwrap_err()[0] { @@ -1253,8 +1273,12 @@ fn cannot_filter_by_derved_relationship_fields() { fn subscription_gets_result_even_without_events() { run_test_sequentially(setup, |_, id| async move { let logger = Logger::root(slog::Discard, o!()); - let store = STORE.clone().query_store(id.clone().into(), true).unwrap(); - let schema = STORE.api_schema(&id).unwrap(); + let store = STORE + .clone() + .query_store(id.clone().into(), true) + .await + .unwrap(); + let schema = STORE.subgraph_store().api_schema(&id).unwrap(); let query = Query::new( graphql_parser::parse_query( @@ -1533,7 +1557,9 @@ fn query_detects_reorg() { .expect("invalid test query") .into_static(); let state = STORE + .subgraph_store() .deployment_state_from_id(id.clone()) + .await .expect("failed to get state"); // Inject a fake initial state; c435c25decbc4ad7bbbadf8e0ced0ff2 @@ -1551,6 +1577,7 @@ fn query_detects_reorg() { // Revert one block STORE + .subgraph_store() .revert_block_operations(id.clone(), GENESIS_PTR.clone()) .unwrap(); // A query is still fine since we implicitly query at block 0; we were @@ -1566,7 +1593,13 @@ fn query_detects_reorg() { // We move the subgraph head forward, which will execute the query at block 1 // But the state we have is also for block 1, but with a smaller reorg count // and we therefore report an error - transact_entity_operations(&*STORE, id.clone(), BLOCK_ONE.clone(), vec![]).unwrap(); + transact_entity_operations( + &STORE.subgraph_store(), + id.clone(), + BLOCK_ONE.clone(), + vec![], + ) + .unwrap(); let result = execute_query_document(&id, query.clone()).await; match result.to_result().unwrap_err()[0] { QueryError::ExecutionError(QueryExecutionError::DeploymentReverted) => { /* expected */ @@ -1732,6 +1765,7 @@ fn non_fatal_errors() { // Test error reverts. STORE + .subgraph_store() .revert_block_operations(id.clone(), *BLOCK_ONE) .unwrap(); let query = "query { musician(id: \"m1\") { id } _meta { hasIndexingErrors } }"; diff --git a/mock/src/store.rs b/mock/src/store.rs index d7de9deaf6a..4d2073cd5a1 100644 --- a/mock/src/store.rs +++ b/mock/src/store.rs @@ -2,10 +2,10 @@ use mockall::predicate::*; use mockall::*; use std::collections::BTreeMap; -use graph::components::{server::index_node::VersionInfo, store::StoredDynamicDataSource}; +use graph::components::store::EntityType; +use graph::components::store::StoredDynamicDataSource; use graph::data::subgraph::schema::SubgraphError; use graph::prelude::*; -use graph::{components::store::EntityType, data::subgraph::status}; use web3::types::{Address, H256}; mock! { @@ -55,7 +55,7 @@ mock! { } #[async_trait] -impl Store for MockStore { +impl SubgraphStore for MockStore { fn block_ptr( &self, _subgraph_id: &SubgraphDeploymentId, @@ -122,11 +122,14 @@ impl Store for MockStore { unimplemented!() } - fn deployment_state_from_name(&self, _: SubgraphName) -> Result { + async fn deployment_state_from_name( + &self, + _: SubgraphName, + ) -> Result { unimplemented!() } - fn deployment_state_from_id( + async fn deployment_state_from_id( &self, id: SubgraphDeploymentId, ) -> Result { @@ -182,15 +185,15 @@ impl Store for MockStore { unimplemented!() } - fn is_deployment_synced(&self, _: &SubgraphDeploymentId) -> Result { + fn unfail(&self, _: &SubgraphDeploymentId) -> Result<(), StoreError> { unimplemented!() } - fn deployment_synced(&self, _: &SubgraphDeploymentId) -> Result<(), Error> { + fn is_deployment_synced(&self, _: &SubgraphDeploymentId) -> Result { unimplemented!() } - fn status(&self, _: status::Filter) -> Result, StoreError> { + fn deployment_synced(&self, _: &SubgraphDeploymentId) -> Result<(), Error> { unimplemented!() } @@ -221,18 +224,7 @@ impl Store for MockStore { unimplemented!() } - fn network_name(&self, _: &SubgraphDeploymentId) -> Result, StoreError> { - unimplemented!() - } - - fn version_info(&self, _: &str) -> Result { - unimplemented!() - } - - fn versions_for_subgraph_id( - &self, - _: &str, - ) -> Result<(Option, Option), StoreError> { + fn network_name(&self, _: &SubgraphDeploymentId) -> Result { unimplemented!() } } diff --git a/node/Cargo.toml b/node/Cargo.toml index b91a605dc2d..a29fbe8b862 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -18,10 +18,10 @@ env_logger = "0.8.2" git-testament = "0.1" graphql-parser = "0.3" prometheus = "0.7" -futures = { version = "0.3.1", features = ["compat"] } -ipfs-api = { version = "=0.7.1", features = ["hyper-tls"] } -lazy_static = "1.2.0" -url = "2.1.1" +futures = { version = "0.3.12", features = ["compat"] } +ipfs-api = "0.7.2" +lazy_static = "1.4.0" +url = "2.2.0" crossbeam-channel = "0.5.0" graph = { path = "../graph" } graph-core = { path = "../core" } @@ -36,7 +36,7 @@ graph-server-websocket = { path = "../server/websocket" } graph-server-metrics = { path = "../server/metrics" } graph-store-postgres = { path = "../store/postgres" } regex = "1.4.2" -serde = { version = "1.0.117", features = ["derive", "rc"] } +serde = { version = "1.0.123", features = ["derive", "rc"] } serde_regex = "1.1.0" structopt = "0.3.20" toml = "0.5.7" diff --git a/node/src/bin/manager.rs b/node/src/bin/manager.rs index 4bedfc17f5e..4392feb4252 100644 --- a/node/src/bin/manager.rs +++ b/node/src/bin/manager.rs @@ -1,20 +1,20 @@ use std::{env, sync::Arc}; use git_testament::{git_testament, render_testament}; +use graph::prometheus::Registry; use graph_core::MetricsRegistry; use lazy_static::lazy_static; -use prometheus::Registry; use structopt::StructOpt; use graph::{ log::logger, - prelude::{info, o, slog, tokio, Logger}, + prelude::{info, o, slog, tokio, Logger, NodeId}, }; use graph_node::config; use graph_node::store_builder::StoreBuilder; -use graph_store_postgres::{connection_pool::ConnectionPool, ShardedStore, PRIMARY_SHARD}; +use graph_store_postgres::{connection_pool::ConnectionPool, SubgraphStore, PRIMARY_SHARD}; -use crate::config::Config; +use crate::config::Config as Cfg; use graph_node::manager::commands; git_testament!(TESTAMENT); @@ -41,10 +41,19 @@ lazy_static! { pub struct Opt { #[structopt( long, + short, env = "GRAPH_NODE_CONFIG", help = "the name of the configuration file" )] pub config: String, + #[structopt( + long, + default_value = "default", + value_name = "NODE_ID", + env = "GRAPH_NODE_ID", + help = "a unique identifier for this node" + )] + pub node_id: String, #[structopt(subcommand)] pub cmd: Command, } @@ -70,13 +79,33 @@ pub enum Command { #[structopt(long, short)] used: bool, }, - /// Print how a specific subgraph would be placed - Place { name: String, network: String }, /// Manage unused deployments /// /// Record which deployments are unused with `record`, then remove them /// with `remove` Unused(UnusedCommand), + /// Remove a named subgraph + Remove { + /// The name of the subgraph to remove + name: String, + }, + /// Assign or reassign a deployment + Reassign { + /// The id of the deployment to reassign + id: String, + /// The name of the node that should index the deployment + node: String, + }, + /// Unassign a deployment + Unassign { + /// The id of the deployment to unassign + id: String, + }, + /// Check and interrogate the configuration + /// + /// Print information about a configuration file without + /// actually connecting to databases or network clients + Config(ConfigCommand), } #[derive(Clone, Debug, StructOpt)] @@ -103,6 +132,31 @@ pub enum UnusedCommand { }, } +#[derive(Clone, Debug, StructOpt)] +pub enum ConfigCommand { + /// Check and validate the configuration file + Check { + /// Print the configuration as JSON + #[structopt(long)] + print: bool, + }, + /// Print how a specific subgraph would be placed + Place { + /// The name of the subgraph + name: String, + /// The network the subgraph indexes + network: String, + }, + /// Information about the size of database pools + Pools { + /// The names of the nodes that are going to run + nodes: Vec, + /// Print connections by shard rather than by node + #[structopt(short, long)] + shard: bool, + }, +} + impl From for config::Opt { fn from(opt: Opt) -> Self { let mut config_opt = config::Opt::default(); @@ -120,18 +174,19 @@ fn make_registry(logger: &Logger) -> Arc { )) } -fn make_main_pool(logger: &Logger, config: &Config) -> ConnectionPool { +fn make_main_pool(logger: &Logger, node_id: &NodeId, config: &Cfg) -> ConnectionPool { let primary = config.primary_store(); StoreBuilder::main_pool( &logger, + node_id, PRIMARY_SHARD.as_str(), primary, make_registry(logger), ) } -fn make_store(logger: &Logger, config: &Config) -> Arc { - StoreBuilder::make_sharded_store(logger, config, make_registry(logger)) +fn make_store(logger: &Logger, node_id: &NodeId, config: &Cfg) -> Arc { + StoreBuilder::make_sharded_store(logger, node_id, config, make_registry(logger)) } #[tokio::main] @@ -151,18 +206,27 @@ async fn main() { render_testament!(TESTAMENT) ); - let config = match Config::load(&logger, &opt.clone().into()) { + let config = match Cfg::load(&logger, &opt.clone().into()) { Err(e) => { eprintln!("configuration error: {}", e); std::process::exit(1); } Ok(config) => config, }; + let node = match NodeId::new(&opt.node_id) { + Err(()) => { + eprintln!("invalid node id: {}", opt.node_id); + std::process::exit(1); + } + Ok(node) => node, + }; + let make_main_pool = || make_main_pool(&logger, &node, &config); + let make_store = || make_store(&logger, &node, &config); use Command::*; let result = match opt.cmd { TxnSpeed { delay } => { - let pool = make_main_pool(&logger, &config); + let pool = make_main_pool(); commands::txn_speed::run(pool, delay) } Info { @@ -171,12 +235,11 @@ async fn main() { pending, used, } => { - let pool = make_main_pool(&logger, &config); + let pool = make_main_pool(); commands::info::run(pool, name, current, pending, used) } - Place { name, network } => commands::place::run(&config.deployment, &name, &network), Unused(cmd) => { - let store = make_store(&logger, &config); + let store = make_store(); use UnusedCommand::*; match cmd { @@ -188,6 +251,29 @@ async fn main() { } } } + Config(cmd) => { + use ConfigCommand::*; + + match cmd { + Place { name, network } => { + commands::config::place(&config.deployment, &name, &network) + } + Check { print } => commands::config::check(&config, print), + Pools { nodes, shard } => commands::config::pools(&config, nodes, shard), + } + } + Remove { name } => { + let store = make_store(); + commands::remove::run(store, name) + } + Unassign { id } => { + let store = make_store(); + commands::assign::unassign(store, id) + } + Reassign { id, node } => { + let store = make_store(); + commands::assign::reassign(store, id, node) + } }; if let Err(e) = result { die!("error: {}", e) diff --git a/node/src/config.rs b/node/src/config.rs index 61e78d319db..33e0dd73db6 100644 --- a/node/src/config.rs +++ b/node/src/config.rs @@ -1,6 +1,9 @@ -use graph::prelude::{ - anyhow::{anyhow, Result}, - info, serde_json, Logger, NodeId, +use graph::{ + components::ethereum::NodeCapabilities, + prelude::{ + anyhow::{anyhow, bail, Context, Result}, + info, serde_json, Logger, NodeId, + }, }; use graph_chain_ethereum::CLEANUP_BLOCKS; use graph_store_postgres::{DeploymentPlacer, Shard as ShardName, PRIMARY_SHARD}; @@ -12,15 +15,23 @@ use std::fs::read_to_string; use url::Url; const ANY_NAME: &str = ".*"; +/// A regular expression that matches nothing +const NO_NAME: &str = ".^"; pub struct Opt { pub postgres_url: Option, pub config: Option, + // This is only used when we cosntruct a config purely from command + // line options. When using a configuration file, pool sizes must be + // set in the configuration file alone pub store_connection_pool_size: u32, pub postgres_secondary_hosts: Vec, pub postgres_host_weights: Vec, pub disable_block_ingestor: bool, pub node_id: String, + pub ethereum_rpc: Vec, + pub ethereum_ws: Vec, + pub ethereum_ipc: Vec, } impl Default for Opt { @@ -33,19 +44,23 @@ impl Default for Opt { postgres_host_weights: vec![], disable_block_ingestor: true, node_id: "default".to_string(), + ethereum_rpc: vec![], + ethereum_ws: vec![], + ethereum_ipc: vec![], } } } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Config { + pub general: Option, #[serde(rename = "store")] pub stores: BTreeMap, + pub chains: ChainSection, pub deployment: Deployment, - ingestor: Ingestor, } -fn validate_replica_name(s: &str) -> Result<()> { +fn validate_name(s: &str) -> Result<()> { if s.is_empty() { return Err(anyhow!("names must not be empty")); } @@ -70,9 +85,8 @@ fn validate_replica_name(s: &str) -> Result<()> { } impl Config { - /// Check that the config is valid. Some defaults (like `pool_size`) will - /// be filled in from `opt` at the same time. - fn validate(&mut self, opt: &Opt) -> Result<()> { + /// Check that the config is valid. + fn validate(&mut self) -> Result<()> { if !self.stores.contains_key(PRIMARY_SHARD.as_str()) { return Err(anyhow!("missing a primary store")); } @@ -83,12 +97,11 @@ impl Config { )); } for (key, shard) in self.stores.iter_mut() { - ShardName::new(key.clone()).map_err(|e| anyhow!(e))?; - shard.validate(opt)?; + shard.validate(&key)?; } self.deployment.validate()?; - // Check that deployment rules only reference existing stores + // Check that deployment rules only reference existing stores and chains for (i, rule) in self.deployment.rules.iter().enumerate() { if !self.stores.contains_key(&rule.shard) { return Err(anyhow!( @@ -97,7 +110,26 @@ impl Config { i )); } + if let Some(network) = &rule.pred.network { + if !self.chains.chains.contains_key(network) { + return Err(anyhow!( + "unknown network {} in deployment rule {}", + network, + i + )); + } + } + } + + // Check that chains only reference existing stores + for (name, chain) in &self.chains.chains { + if !self.stores.contains_key(&chain.shard) { + return Err(anyhow!("unknown shard {} in chain {}", chain.shard, name)); + } } + + self.chains.validate()?; + Ok(()) } @@ -108,7 +140,7 @@ impl Config { info!(logger, "Reading configuration file `{}`", config); let config = read_to_string(config)?; let mut config: Config = toml::from_str(&config)?; - config.validate(opt)?; + config.validate()?; Ok(config) } else { info!( @@ -120,14 +152,15 @@ impl Config { } fn from_opt(opt: &Opt) -> Result { - let ingestor = Ingestor::from_opt(opt); let deployment = Deployment::from_opt(opt); let mut stores = BTreeMap::new(); + let chains = ChainSection::from_opt(opt)?; stores.insert(PRIMARY_SHARD.to_string(), Shard::from_opt(opt)?); Ok(Config { + general: None, stores, + chains, deployment, - ingestor, }) } @@ -146,6 +179,22 @@ impl Config { .get(PRIMARY_SHARD.as_str()) .expect("a validated config has a primary store") } + + pub fn query_only(&self, node: &NodeId) -> bool { + self.general + .as_ref() + .map(|g| match g.query.find(node.as_str()) { + None => false, + Some(m) => m.as_str() == node.as_str(), + }) + .unwrap_or(false) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct GeneralSection { + #[serde(with = "serde_regex", default = "no_name")] + query: Regex, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -154,34 +203,25 @@ pub struct Shard { #[serde(default = "one")] pub weight: usize, #[serde(default)] - pub pool_size: u32, + pub pool_size: PoolSize, #[serde(default)] pub replicas: BTreeMap, } -fn check_pool_size(pool_size: u32, connection: &str) -> Result<()> { - if pool_size < 2 { - Err(anyhow!( - "connection pool size must be at least 2, but is {} for {}", - pool_size, - connection - )) - } else { - Ok(()) - } -} - impl Shard { - fn validate(&mut self, opt: &Opt) -> Result<()> { + fn validate(&mut self, name: &str) -> Result<()> { + ShardName::new(name.to_string()).map_err(|e| anyhow!(e))?; + self.connection = shellexpand::env(&self.connection)?.into_owned(); - if self.pool_size == 0 { - self.pool_size = opt.store_connection_pool_size; + + if matches!(self.pool_size, PoolSize::None) { + return Err(anyhow!("missing pool size definition for shard `{}`", name)); } - check_pool_size(self.pool_size, &self.connection)?; + self.pool_size.validate(&self.connection)?; for (name, replica) in self.replicas.iter_mut() { - validate_replica_name(name)?; - replica.validate(opt)?; + validate_name(name).context("illegal replica name")?; + replica.validate(&self.pool_size)?; } Ok(()) } @@ -191,44 +231,303 @@ impl Shard { .postgres_url .as_ref() .expect("validation checked that postgres_url is set"); - check_pool_size(opt.store_connection_pool_size, &postgres_url)?; + let pool_size = PoolSize::Fixed(opt.store_connection_pool_size); + pool_size.validate(&postgres_url)?; let mut replicas = BTreeMap::new(); for (i, host) in opt.postgres_secondary_hosts.iter().enumerate() { let replica = Replica { connection: replace_host(&postgres_url, &host), weight: opt.postgres_host_weights.get(i + 1).cloned().unwrap_or(1), - pool_size: opt.store_connection_pool_size, + pool_size: pool_size.clone(), }; replicas.insert(format!("replica{}", i + 1), replica); } Ok(Self { connection: postgres_url.clone(), weight: opt.postgres_host_weights.get(0).cloned().unwrap_or(1), - pool_size: opt.store_connection_pool_size, + pool_size, replicas, }) } } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum PoolSize { + None, + Fixed(u32), + Rule(Vec), +} + +impl Default for PoolSize { + fn default() -> Self { + Self::None + } +} + +impl PoolSize { + fn validate(&self, connection: &str) -> Result<()> { + use PoolSize::*; + + let pool_size = match self { + None => bail!("missing pool size for {}", connection), + Fixed(s) => s.clone(), + Rule(rules) => rules.iter().map(|rule| rule.size).min().unwrap_or(0u32), + }; + + if pool_size < 2 { + Err(anyhow!( + "connection pool size must be at least 2, but is {} for {}", + pool_size, + connection + )) + } else { + Ok(()) + } + } + + pub fn size_for(&self, node: &NodeId, name: &str) -> Result { + use PoolSize::*; + match self { + None => unreachable!("validation ensures we have a pool size"), + Fixed(s) => Ok(s.clone()), + Rule(rules) => rules + .iter() + .find(|rule| rule.matches(node.as_str())) + .map(|rule| rule.size) + .ok_or_else(|| { + anyhow!( + "no rule matches `{}` for the pool of shard {}", + node.as_str(), + name + ) + }), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PoolSizeRule { + #[serde(with = "serde_regex", default = "any_name")] + node: Regex, + size: u32, +} + +impl PoolSizeRule { + fn matches(&self, name: &str) -> bool { + match self.node.find(name) { + None => false, + Some(m) => m.as_str() == name, + } + } +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Replica { pub connection: String, #[serde(default = "one")] pub weight: usize, - #[serde(default = "zero")] - pub pool_size: u32, + #[serde(default)] + pub pool_size: PoolSize, } impl Replica { - fn validate(&mut self, opt: &Opt) -> Result<()> { + fn validate(&mut self, pool_size: &PoolSize) -> Result<()> { self.connection = shellexpand::env(&self.connection)?.into_owned(); - if self.pool_size == 0 { - self.pool_size = opt.store_connection_pool_size; + if matches!(self.pool_size, PoolSize::None) { + self.pool_size = pool_size.clone(); + } + + self.pool_size.validate(&self.connection)?; + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ChainSection { + pub ingestor: String, + #[serde(flatten)] + pub chains: BTreeMap, +} + +impl ChainSection { + fn validate(&self) -> Result<()> { + NodeId::new(&self.ingestor) + .map_err(|()| anyhow!("invalid node id for ingestor {}", &self.ingestor))?; + for (_, chain) in &self.chains { + chain.validate()? + } + Ok(()) + } + + fn from_opt(opt: &Opt) -> Result { + // If we are not the block ingestor, set the node name + // to something that is definitely not our node_id + let ingestor = if opt.disable_block_ingestor { + format!("{} is not ingesting", opt.node_id) + } else { + opt.node_id.clone() + }; + let mut chains = BTreeMap::new(); + Self::parse_networks(&mut chains, Transport::Rpc, &opt.ethereum_rpc)?; + Self::parse_networks(&mut chains, Transport::Ws, &opt.ethereum_ws)?; + Self::parse_networks(&mut chains, Transport::Ipc, &opt.ethereum_ipc)?; + Ok(Self { ingestor, chains }) + } + + fn parse_networks( + chains: &mut BTreeMap, + transport: Transport, + args: &Vec, + ) -> Result<()> { + for (nr, arg) in args.iter().enumerate() { + if arg.starts_with("wss://") + || arg.starts_with("http://") + || arg.starts_with("https://") + { + return Err(anyhow!( + "Is your Ethereum node string missing a network name? \ + Try 'mainnet:' + the Ethereum node URL." + )); + } else { + // Parse string (format is "NETWORK_NAME:NETWORK_CAPABILITIES:URL" OR + // "NETWORK_NAME::URL" which will default to NETWORK_CAPABILITIES="archive,traces") + let colon = arg.find(':').ok_or_else(|| { + return anyhow!( + "A network name must be provided alongside the \ + Ethereum node location. Try e.g. 'mainnet:URL'." + ); + })?; + + let (name, rest_with_delim) = arg.split_at(colon); + let rest = &rest_with_delim[1..]; + if name.is_empty() { + return Err(anyhow!("Ethereum network name cannot be an empty string")); + } + if rest.is_empty() { + return Err(anyhow!("Ethereum node URL cannot be an empty string")); + } + + let colon = rest.find(":").ok_or_else(|| { + return anyhow!( + "A network name must be provided alongside the \ + Ethereum node location. Try e.g. 'mainnet:URL'." + ); + })?; + + let (features, url_str) = rest.split_at(colon); + let (url, features) = if vec!["http", "https", "ws", "wss"].contains(&features) { + (rest, PROVIDER_FEATURES.to_vec()) + } else { + (&url_str[1..], features.split(',').collect()) + }; + let features = features.into_iter().map(|s| s.to_string()).collect(); + let provider = Provider { + label: format!("{}-{}-{}", name, transport, nr), + transport, + url: url.to_string(), + features, + }; + let entry = chains.entry(name.to_string()).or_insert_with(|| Chain { + shard: PRIMARY_SHARD.to_string(), + providers: vec![], + }); + entry.providers.push(provider); + } + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Chain { + pub shard: String, + #[serde(rename = "provider")] + pub providers: Vec, +} + +impl Chain { + fn validate(&self) -> Result<()> { + // `Config` validates that `self.shard` references a configured shard + + for provider in &self.providers { + provider.validate()? + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Provider { + pub label: String, + #[serde(default)] + pub transport: Transport, + pub url: String, + pub features: Vec, +} + +const PROVIDER_FEATURES: [&str; 2] = ["traces", "archive"]; + +impl Provider { + fn validate(&self) -> Result<()> { + validate_name(&self.label).context("illegal provider name")?; + + for feature in &self.features { + if !PROVIDER_FEATURES.contains(&feature.as_str()) { + return Err(anyhow!( + "illegal feature `{}` for provider {}. Features must be one of {}", + feature, + self.label, + PROVIDER_FEATURES.join(", ") + )); + } } - check_pool_size(self.pool_size, &self.connection)?; + Url::parse(&self.url).map_err(|e| { + anyhow!( + "the url `{}` for provider {} is not a legal URL: {}", + self.url, + self.label, + e + ) + })?; Ok(()) } + + pub fn node_capabilities(&self) -> NodeCapabilities { + NodeCapabilities { + archive: self.features.iter().any(|f| f == "archive"), + traces: self.features.iter().any(|f| f == "traces"), + } + } +} + +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +pub enum Transport { + #[serde(rename = "rpc")] + Rpc, + #[serde(rename = "ws")] + Ws, + #[serde(rename = "ipc")] + Ipc, +} + +impl Default for Transport { + fn default() -> Self { + Self::Rpc + } +} + +impl std::fmt::Display for Transport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Transport::*; + + match self { + Rpc => write!(f, "rpc"), + Ws => write!(f, "ws"), + Ipc => write!(f, "ipc"), + } + } } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -356,27 +655,6 @@ impl Default for Predicate { } } -#[derive(Clone, Debug, Deserialize, Serialize)] -struct Ingestor { - node: String, -} - -impl Ingestor { - fn from_opt(opt: &Opt) -> Self { - // If we are not the block ingestor, set the node name - // to something that is definitely not our node_id - if opt.disable_block_ingestor { - Ingestor { - node: format!("{} is not ingesting", opt.node_id), - } - } else { - Ingestor { - node: opt.node_id.clone(), - } - } - } -} - /// Replace the host portion of `url` and return a new URL with `host` /// as the host portion /// @@ -399,6 +677,10 @@ fn any_name() -> Regex { Regex::new(ANY_NAME).unwrap() } +fn no_name() -> Regex { + Regex::new(NO_NAME).unwrap() +} + fn primary_store() -> String { PRIMARY_SHARD.to_string() } @@ -406,7 +688,3 @@ fn primary_store() -> String { fn one() -> usize { 1 } - -fn zero() -> u32 { - 0 -} diff --git a/node/src/main.rs b/node/src/main.rs index 0f3a176db78..998df2d0a71 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -1,8 +1,7 @@ use git_testament::{git_testament, render_testament}; +use graph::prometheus::Registry; use ipfs_api::IpfsClient; use lazy_static::lazy_static; -use prometheus::Registry; -use std::collections::HashMap; use std::env; use std::io::{BufRead, BufReader}; use std::path::Path; @@ -11,8 +10,11 @@ use std::time::Duration; use structopt::StructOpt; use tokio::sync::mpsc; -use graph::components::ethereum::{EthereumNetworks, NodeCapabilities}; use graph::components::forward; +use graph::components::{ + ethereum::{EthereumNetworks, NodeCapabilities}, + store::BlockStore, +}; use graph::data::graphql::effort::LoadManager; use graph::log::logger; use graph::prelude::{IndexNodeServer as _, JsonRpcServer as _, *}; @@ -31,7 +33,7 @@ use graph_server_index_node::IndexNodeServer; use graph_server_json_rpc::JsonRpcServer; use graph_server_metrics::PrometheusMetricsServer; use graph_server_websocket::SubscriptionServer as GraphQLSubscriptionServer; -use graph_store_postgres::NetworkStore as DieselNetworkStore; +use graph_store_postgres::BlockStore as DieselBlockStore; mod config; mod opt; @@ -58,13 +60,6 @@ lazy_static! { git_testament!(TESTAMENT); -#[derive(Debug, Clone)] -enum ConnectionType { - IPC, - RPC, - WS, -} - fn read_expensive_queries() -> Result>, std::io::Error> { // A file with a list of expensive queries, one query per line // Attempts to run these queries will return a @@ -101,6 +96,13 @@ fn read_expensive_queries() -> Result>, std::io::Error> { async fn main() { env_logger::init(); + let client = IpfsClient::default(); + + match client.version().await { + Ok(version) => eprintln!("version: {:?}", version.version), + Err(e) => eprintln!("error getting version: {}", e), + } + // Allow configuring fail points on debug builds. Used for integration tests. #[cfg(debug_assertions)] std::mem::forget(fail::FailScenario::setup()); @@ -135,6 +137,7 @@ async fn main() { let node_id = NodeId::new(opt.node_id.clone()).expect("Node ID must contain only a-z, A-Z, 0-9, and '_'"); + let query_only = config.query_only(&node_id); // Obtain subgraph related command-line arguments let subgraph = opt.subgraph.clone(); @@ -189,15 +192,15 @@ async fn main() { let mut metrics_server = PrometheusMetricsServer::new(&logger_factory, prometheus_registry.clone()); - // Ethereum clients - let eth_networks = create_ethereum_networks( - &logger, - metrics_registry.clone(), - &opt.ethereum_rpc, - &opt.ethereum_ipc, - &opt.ethereum_ws, - ) - .await; + // Ethereum clients; query nodes ignore all ethereum clients and never + // connect to them directly + let eth_networks = if query_only { + EthereumNetworks::new() + } else { + create_ethereum_networks(logger.clone(), metrics_registry.clone(), &config) + .await + .expect("Failed to parse Ethereum networks") + }; let graphql_metrics_registry = metrics_registry.clone(); @@ -208,12 +211,8 @@ async fn main() { let expensive_queries = read_expensive_queries().unwrap(); - let store_builder = Arc::new(StoreBuilder::new( - &logger, - &config, - metrics_registry.cheap_clone(), - )); - let store_builder2 = store_builder.clone(); + let store_builder = + StoreBuilder::new(&logger, &node_id, &config, metrics_registry.cheap_clone()); graph::spawn( futures::stream::FuturesOrdered::from_iter(stores_eth_networks.flatten().into_iter().map( @@ -242,24 +241,22 @@ async fn main() { "network_version" => &network_identifier.net_version, "capabilities" => &capabilities ); - let network_store = - store_builder2.network_store(network_name.clone(), network_identifier); - (network_name.to_string(), network_store) + (network_name, network_identifier) }) .collect() - .map(|stores| HashMap::from_iter(stores.into_iter())) - .and_then(move |network_stores| { + .and_then(move |networks| { + let subscription_manager = store_builder.subscription_manager(); + let network_store = store_builder.network_store(networks); let load_manager = Arc::new(LoadManager::new( &logger, expensive_queries, metrics_registry.clone(), store_conn_pool_size as usize, )); - let generic_network_store = network_stores.values().next().unwrap().clone(); let graphql_runner = Arc::new(GraphQlRunner::new( &logger, - generic_network_store, - store_builder.subscription_manager(), + network_store.clone(), + subscription_manager.clone(), load_manager, )); let mut graphql_server = GraphQLQueryServer::new( @@ -271,13 +268,13 @@ async fn main() { let subscription_server = GraphQLSubscriptionServer::new( &logger, graphql_runner.clone(), - store_builder.store(), + network_store.subgraph_store(), ); let mut index_node_server = IndexNodeServer::new( &logger_factory, graphql_runner.clone(), - store_builder.store(), + network_store.clone(), ); // Spawn Ethereum network indexers for all networks that are to be indexed @@ -298,10 +295,7 @@ async fn main() { ) .expect(&*format!("adapter for network, {}", network_name)) .clone(), - network_stores - .get(&network_name) - .expect("store for network") - .clone(), + network_store.subgraph_store(), metrics_registry.clone(), format!("network/{}", network_subgraph).into(), None, @@ -327,14 +321,14 @@ async fn main() { &logger, block_polling_interval, ð_networks, - &network_stores, + network_store.block_store(), &logger_factory, ); } let block_stream_builder = BlockStreamBuilder::new( - store_builder.store(), - network_stores.clone(), + network_store.subgraph_store(), + network_store.block_store(), eth_networks.clone(), node_id.clone(), *REORG_THRESHOLD, @@ -343,14 +337,16 @@ async fn main() { let runtime_host_builder = WASMRuntimeHostBuilder::new( eth_networks.clone(), link_resolver.clone(), - network_stores.clone(), + network_store.subgraph_store(), + network_store.block_store(), arweave_adapter, three_box_adapter, ); let subgraph_instance_manager = SubgraphInstanceManager::new( &logger_factory, - network_stores.clone(), + network_store.subgraph_store(), + network_store.block_store(), eth_networks.clone(), runtime_host_builder, block_stream_builder, @@ -361,7 +357,7 @@ async fn main() { let mut subgraph_provider = IpfsSubgraphAssignmentProvider::new( &logger_factory, link_resolver.clone(), - store_builder.store(), + network_store.subgraph_store(), ); // Forward subgraph events from the subgraph provider to the subgraph instance manager @@ -384,9 +380,9 @@ async fn main() { &logger_factory, link_resolver, Arc::new(subgraph_provider), - store_builder.store(), - store_builder.subscription_manager(), - network_stores, + network_store.subgraph_store(), + subscription_manager, + network_store.block_store(), eth_networks.clone(), node_id.clone(), version_switching_mode, @@ -503,80 +499,31 @@ async fn main() { } /// Parses an Ethereum connection string and returns the network name and Ethereum adapter. -async fn parse_ethereum_networks( +async fn create_ethereum_networks( logger: Logger, - networks: Vec, - connection_type: ConnectionType, registry: Arc, + config: &Config, ) -> Result { let eth_rpc_metrics = Arc::new(ProviderEthRpcMetrics::new(registry)); let mut parsed_networks = EthereumNetworks::new(); - for network_arg in networks { - if network_arg.starts_with("wss://") - || network_arg.starts_with("http://") - || network_arg.starts_with("https://") - { - return Err(anyhow::anyhow!( - "Is your Ethereum node string missing a network name? \ - Try 'mainnet:' + the Ethereum node URL." - )); - } else { - // Parse string (format is "NETWORK_NAME:NETWORK_CAPABILITIES:URL" OR - // "NETWORK_NAME::URL" which will default to NETWORK_CAPABILITIES="archive,traces") - let split_at = network_arg.find(':').ok_or_else(|| { - return anyhow::anyhow!( - "A network name must be provided alongside the \ - Ethereum node location. Try e.g. 'mainnet:URL'." - ); - })?; - - let (name, rest_with_delim) = network_arg.split_at(split_at); - let rest = &rest_with_delim[1..]; - if name.is_empty() { - return Err(anyhow::anyhow!( - "Ethereum network name cannot be an empty string" - )); - } - - let url_split_at = rest.find(":").ok_or_else(|| { - return anyhow::anyhow!( - "A network name must be provided alongside the \ - Ethereum node location. Try e.g. 'mainnet:URL'." - ); - })?; - - let (capabilities_str, url_str) = rest.split_at(url_split_at); - let (url, capabilities) = - if vec!["http", "https", "ws", "wss"].contains(&capabilities_str) { - ( - rest, - NodeCapabilities { - archive: true, - traces: true, - }, - ) - } else { - (&url_str[1..], capabilities_str.parse()?) - }; - - if rest.is_empty() { - return Err(anyhow::anyhow!( - "Ethereum node URL cannot be an empty string" - )); - } + for (name, chain) in &config.chains.chains { + for provider in &chain.providers { + let capabilities = provider.node_capabilities(); info!( logger, "Creating transport"; "network" => &name, - "url" => &url, + "url" => &provider.url, "capabilities" => capabilities ); - let (transport_event_loop, transport) = match connection_type { - ConnectionType::RPC => Transport::new_rpc(url), - ConnectionType::IPC => Transport::new_ipc(url), - ConnectionType::WS => Transport::new_ws(url), + use crate::config::Transport::*; + + let (transport_event_loop, transport) = match provider.transport { + Rpc => Transport::new_rpc(&provider.url), + Ipc => Transport::new_ipc(&provider.url), + Ws => Transport::new_ws(&provider.url), }; // If we drop the event loop the transport will stop working. @@ -588,7 +535,7 @@ async fn parse_ethereum_networks( capabilities, Arc::new( graph_chain_ethereum::EthereumAdapter::new( - url, + &provider.url, transport, eth_rpc_metrics.clone(), ) @@ -639,15 +586,29 @@ fn create_ipfs_clients(logger: &Logger, ipfs_addresses: &Vec) -> Vec info!("version: {:?}", version.version), + // Err(e) => eprintln!("error getting version: {}", e), + // } + + graph::spawn(async move { ipfs_test .version() .map_err(move |e| { error!( ipfs_err_logger, - "Is there an IPFS node running at \"{}\"?", + "Is there an IPFS node running at \"{}\" realy?", SafeDisplay(ipfs_address_for_err), ); panic!("Failed to connect to IPFS: {}", e); @@ -667,44 +628,11 @@ fn create_ipfs_clients(logger: &Logger, ipfs_addresses: &Vec) -> Vec, - rpc: &Vec, - ipc: &Vec, - ws: &Vec, -) -> EthereumNetworks { - let mut eth_networks = EthereumNetworks::new(); - - for (connection_type, values) in [ - (ConnectionType::RPC, rpc), - (ConnectionType::IPC, ipc), - (ConnectionType::WS, ws), - ] - .iter() - .cloned() - .filter(|(_, values)| values.len() > 0) - { - let networks = parse_ethereum_networks( - logger.clone(), - values.clone(), - connection_type, - metrics_registry.clone(), - ) - .await - .expect("Failed to parse Ethereum networks"); - - eth_networks.extend(networks); - } - eth_networks.sort(); - eth_networks -} - fn start_block_ingestor( logger: &Logger, block_polling_interval: Duration, eth_networks: &EthereumNetworks, - network_stores: &HashMap>, + block_store: Arc, logger_factory: &LoggerFactory, ) { // BlockIngestor must be configured to keep at least REORG_THRESHOLD ancestors, @@ -727,10 +655,9 @@ fn start_block_ingestor( ); let eth_adapter = eth_adapters.cheapest().unwrap(); //Safe to unwrap since it cannot be empty let block_ingestor = BlockIngestor::new( - network_stores - .get(network_name) - .expect("network with name") - .clone(), + block_store + .chain_store(network_name) + .expect("network with name"), eth_adapter.clone(), *ANCESTOR_COUNT, network_name.to_string(), @@ -746,13 +673,13 @@ fn start_block_ingestor( #[cfg(test)] mod test { - use super::parse_ethereum_networks; - use crate::ConnectionType; + use super::create_ethereum_networks; + use crate::config::{Config, Opt}; use graph::components::ethereum::NodeCapabilities; use graph::log::logger; use graph::prelude::tokio; + use graph::prometheus::Registry; use graph_core::MetricsRegistry; - use prometheus::Registry; use std::sync::Arc; #[tokio::test] @@ -764,16 +691,29 @@ mod test { "goerli:archive:http://localhost:8546/".to_string(), ]; + let opt = Opt { + postgres_url: Some("not needed".to_string()), + config: None, + store_connection_pool_size: 5, + postgres_secondary_hosts: vec![], + postgres_host_weights: vec![], + disable_block_ingestor: true, + node_id: "default".to_string(), + ethereum_rpc: network_args, + ethereum_ws: vec![], + ethereum_ipc: vec![], + }; + + let config = Config::load(&logger, &opt).expect("can create config"); let prometheus_registry = Arc::new(Registry::new()); let metrics_registry = Arc::new(MetricsRegistry::new( logger.clone(), prometheus_registry.clone(), )); - let ethereum_networks = - parse_ethereum_networks(logger, network_args, ConnectionType::RPC, metrics_registry) - .await - .expect("Correctly parse Ethereum network args"); + let ethereum_networks = create_ethereum_networks(logger, metrics_registry, &config) + .await + .expect("Correctly parse Ethereum network args"); let mut network_names = ethereum_networks.networks.keys().collect::>(); network_names.sort(); diff --git a/node/src/manager/commands/assign.rs b/node/src/manager/commands/assign.rs new file mode 100644 index 00000000000..070e0382c59 --- /dev/null +++ b/node/src/manager/commands/assign.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use graph::prelude::{anyhow, Error, NodeId, SubgraphDeploymentId, SubgraphStore as _}; +use graph_store_postgres::SubgraphStore; + +pub fn unassign(store: Arc, id: String) -> Result<(), Error> { + let id = + SubgraphDeploymentId::new(id).map_err(|id| anyhow!("illegal deployment id `{}`", id))?; + + println!("unassigning {}", id.as_str()); + store.unassign_subgraph(&id)?; + + Ok(()) +} + +pub fn reassign(store: Arc, id: String, node: String) -> Result<(), Error> { + let id = + SubgraphDeploymentId::new(id).map_err(|id| anyhow!("illegal deployment id `{}`", id))?; + let node = NodeId::new(node.clone()).map_err(|()| anyhow!("illegal node id `{}`", node))?; + + println!("reassigning {} to {}", id.as_str(), node.as_str()); + store.reassign_subgraph(&id, &node)?; + + Ok(()) +} diff --git a/node/src/manager/commands/config.rs b/node/src/manager/commands/config.rs new file mode 100644 index 00000000000..6c1b93117f8 --- /dev/null +++ b/node/src/manager/commands/config.rs @@ -0,0 +1,90 @@ +use std::collections::BTreeMap; + +use graph::prelude::{ + anyhow::{anyhow, Error}, + NodeId, +}; +use graph_store_postgres::DeploymentPlacer; + +use crate::config::Config; + +pub fn place(placer: &dyn DeploymentPlacer, name: &str, network: &str) -> Result<(), Error> { + match placer.place(name, network).map_err(|s| anyhow!(s))? { + None => { + println!( + "no matching placement rule; default placement from JSON RPC call would be used" + ); + } + Some((shard, nodes)) => { + let nodes: Vec<_> = nodes.into_iter().map(|n| n.to_string()).collect(); + println!("subgraph: {}", name); + println!("network: {}", network); + println!("shard: {}", shard); + println!("nodes: {}", nodes.join(", ")); + } + } + Ok(()) +} + +pub fn check(config: &Config, print: bool) -> Result<(), Error> { + match config.to_json() { + Ok(txt) => { + if print { + println!("{}", txt); + } else { + println!("Successfully validated configuration"); + } + Ok(()) + } + Err(e) => Err(anyhow!("error serializing config: {}", e)), + } +} + +pub fn pools(config: &Config, nodes: Vec, shard: bool) -> Result<(), Error> { + // Quietly replace `-` with `_` in node names to make passing in pod names + // from k8s less annoying + let nodes: Vec<_> = nodes + .into_iter() + .map(|name| { + NodeId::new(name.replace("-", "_")) + .map_err(|()| anyhow!("illegal node name `{}`", name)) + }) + .collect::>()?; + // node -> shard_name -> size + let mut sizes = BTreeMap::new(); + for node in &nodes { + let mut shard_sizes = BTreeMap::new(); + for (name, shard) in &config.stores { + let size = shard.pool_size.size_for(node, name)?; + shard_sizes.insert(name.to_string(), size); + for (replica_name, replica) in &shard.replicas { + let qname = format!("{}.{}", name, replica_name); + let size = replica.pool_size.size_for(node, &qname)?; + shard_sizes.insert(qname, size); + } + } + sizes.insert(node.to_string(), shard_sizes); + } + + if shard { + let mut by_shard: BTreeMap<&str, u32> = BTreeMap::new(); + for shard_sizes in sizes.values() { + for (shard_name, size) in shard_sizes { + *by_shard.entry(shard_name).or_default() += size; + } + } + for (shard_name, size) in by_shard { + println!("{}: {}", shard_name, size); + } + } else { + for node in &nodes { + let empty = BTreeMap::new(); + println!("{}:", node); + let node_sizes = sizes.get(node.as_str()).unwrap_or(&empty); + for (shard, size) in node_sizes { + println!(" {}: {}", shard, size); + } + } + } + Ok(()) +} diff --git a/node/src/manager/commands/mod.rs b/node/src/manager/commands/mod.rs index eb97fa79417..a7e68609ebf 100644 --- a/node/src/manager/commands/mod.rs +++ b/node/src/manager/commands/mod.rs @@ -1,4 +1,6 @@ +pub mod assign; +pub mod config; pub mod info; -pub mod place; +pub mod remove; pub mod txn_speed; pub mod unused_deployments; diff --git a/node/src/manager/commands/place.rs b/node/src/manager/commands/place.rs deleted file mode 100644 index e5aebe08de1..00000000000 --- a/node/src/manager/commands/place.rs +++ /dev/null @@ -1,20 +0,0 @@ -use graph::prelude::anyhow::{anyhow, Error}; -use graph_store_postgres::DeploymentPlacer; - -pub fn run(placer: &dyn DeploymentPlacer, name: &str, network: &str) -> Result<(), Error> { - match placer.place(name, network).map_err(|s| anyhow!(s))? { - None => { - println!( - "no matching placement rule; default placement from JSON RPC call would be used" - ); - } - Some((shard, nodes)) => { - let nodes: Vec<_> = nodes.into_iter().map(|n| n.to_string()).collect(); - println!("subgraph: {}", name); - println!("network: {}", network); - println!("shard: {}", shard); - println!("nodes: {}", nodes.join(", ")); - } - } - Ok(()) -} diff --git a/node/src/manager/commands/remove.rs b/node/src/manager/commands/remove.rs new file mode 100644 index 00000000000..36d0b614a6d --- /dev/null +++ b/node/src/manager/commands/remove.rs @@ -0,0 +1,14 @@ +use std::sync::Arc; + +use graph::prelude::{anyhow, Error, SubgraphName, SubgraphStore as _}; +use graph_store_postgres::SubgraphStore; + +pub fn run(store: Arc, name: String) -> Result<(), Error> { + let name = SubgraphName::new(name.clone()) + .map_err(|()| anyhow!("illegal subgraph name `{}`", name))?; + + println!("Removing subgraph {}", name); + store.remove_subgraph(name)?; + + Ok(()) +} diff --git a/node/src/manager/commands/unused_deployments.rs b/node/src/manager/commands/unused_deployments.rs index 4e6f9ba5e46..c2f74f19a1a 100644 --- a/node/src/manager/commands/unused_deployments.rs +++ b/node/src/manager/commands/unused_deployments.rs @@ -1,7 +1,7 @@ use std::{sync::Arc, time::Instant}; use graph::prelude::{anyhow::anyhow, anyhow::Error, SubgraphDeploymentId}; -use graph_store_postgres::{unused, ShardedStore, UnusedDeployment}; +use graph_store_postgres::{unused, SubgraphStore, UnusedDeployment}; use crate::manager::display::List; @@ -29,7 +29,7 @@ fn add_row(list: &mut List, deployment: UnusedDeployment) { ]) } -pub fn list(store: Arc, existing: bool) -> Result<(), Error> { +pub fn list(store: Arc, existing: bool) -> Result<(), Error> { let mut list = make_list(); let filter = if existing { @@ -51,7 +51,7 @@ pub fn list(store: Arc, existing: bool) -> Result<(), Error> { Ok(()) } -pub fn record(store: Arc) -> Result<(), Error> { +pub fn record(store: Arc) -> Result<(), Error> { let mut list = make_list(); println!("Recording unused deployments. This might take a while."); @@ -70,7 +70,7 @@ pub fn record(store: Arc) -> Result<(), Error> { } pub fn remove( - store: Arc, + store: Arc, count: usize, deployment: Option, ) -> Result<(), Error> { diff --git a/node/src/manager/deployment.rs b/node/src/manager/deployment.rs index 36a7ffae3e4..87fe82a61d6 100644 --- a/node/src/manager/deployment.rs +++ b/node/src/manager/deployment.rs @@ -14,6 +14,7 @@ pub struct Deployment { pub namespace: String, pub node_id: Option, pub shard: String, + pub chain: String, } impl Deployment { @@ -39,6 +40,7 @@ impl Deployment { ds::name, a::node_id.nullable(), ds::shard, + ds::network, )); let deployments: Vec = if name.starts_with("sgd") { @@ -60,6 +62,7 @@ impl Deployment { "id", "namespace", "shard", + "chain", "node_id", ]); @@ -70,6 +73,7 @@ impl Deployment { deployment.deployment, deployment.namespace, deployment.shard, + deployment.chain, deployment.node_id.unwrap_or("---".to_string()), ]); } diff --git a/node/src/opt.rs b/node/src/opt.rs index 5b9702e0e55..dc1872b12d2 100644 --- a/node/src/opt.rs +++ b/node/src/opt.rs @@ -69,24 +69,24 @@ pub struct Opt { #[structopt( long, min_values=0, - required_unless_one = &["ethereum-ws", "ethereum-ipc"], - conflicts_with_all = &["ethereum-ws", "ethereum-ipc"], + required_unless_one = &["ethereum-ws", "ethereum-ipc", "config"], + conflicts_with_all = &["ethereum-ws", "ethereum-ipc", "config"], value_name="NETWORK_NAME:URL", env="ETHEREUM_RPC", help= "Ethereum network name (e.g. 'mainnet') and Ethereum RPC URL, separated by a ':'", )] pub ethereum_rpc: Vec, #[structopt(long, min_values=0, - required_unless_one = &["ethereum-rpc", "ethereum-ipc"], - conflicts_with_all = &["ethereum-rpc", "ethereum-ipc"], + required_unless_one = &["ethereum-rpc", "ethereum-ipc", "config"], + conflicts_with_all = &["ethereum-rpc", "ethereum-ipc", "config"], value_name="NETWORK_NAME:URL", env="ETHEREUM_WS", help= "Ethereum network name (e.g. 'mainnet') and Ethereum WebSocket URL, separated by a ':'", )] pub ethereum_ws: Vec, #[structopt(long, min_values=0, - required_unless_one = &["ethereum-rpc", "ethereum-ws"], - conflicts_with_all = &["ethereum-rpc", "ethereum-ws"], + required_unless_one = &["ethereum-rpc", "ethereum-ws", "config"], + conflicts_with_all = &["ethereum-rpc", "ethereum-ws", "config"], value_name="NETWORK_NAME:FILE", env="ETHEREUM_IPC", help= "Ethereum network name (e.g. 'mainnet') and Ethereum IPC pipe, separated by a ':'", @@ -170,7 +170,7 @@ pub struct Opt { #[structopt( long, value_name = "MILLISECONDS", - default_value = "1000", + default_value = "300", env = "ETHEREUM_POLLING_INTERVAL", help = "How often to poll the Ethereum node for new blocks" )] @@ -225,6 +225,9 @@ impl From for config::Opt { postgres_secondary_hosts, disable_block_ingestor, node_id, + ethereum_rpc, + ethereum_ws, + ethereum_ipc, .. } = opt; config::Opt { @@ -235,6 +238,9 @@ impl From for config::Opt { postgres_secondary_hosts, disable_block_ingestor, node_id, + ethereum_rpc, + ethereum_ws, + ethereum_ipc, } } } diff --git a/node/src/store_builder.rs b/node/src/store_builder.rs index fe18c944df6..5b5c45c43e8 100644 --- a/node/src/store_builder.rs +++ b/node/src/store_builder.rs @@ -1,50 +1,61 @@ use std::iter::FromIterator; use std::{collections::HashMap, sync::Arc}; -use graph::prelude::{o, MetricsRegistry}; +use graph::prelude::{o, MetricsRegistry, NodeId}; use graph::{ prelude::{info, CheapClone, EthereumNetworkIdentifier, Logger}, util::security::SafeDisplay, }; use graph_store_postgres::connection_pool::ConnectionPool; use graph_store_postgres::{ - ChainHeadUpdateListener as PostgresChainHeadUpdateListener, ChainStore as DieselChainStore, - NetworkStore as DieselNetworkStore, Shard as ShardName, ShardedStore, Store as DieselStore, - SubscriptionManager, PRIMARY_SHARD, + BlockStore as DieselBlockStore, ChainHeadUpdateListener as PostgresChainHeadUpdateListener, + Shard as ShardName, Store as DieselStore, SubgraphStore, SubscriptionManager, PRIMARY_SHARD, }; use crate::config::{Config, Shard}; pub struct StoreBuilder { - store: Arc, - primary_pool: ConnectionPool, - chain_head_update_listener: Arc, + logger: Logger, + subgraph_store: Arc, + pools: HashMap, + primary_shard: Shard, subscription_manager: Arc, + registry: Arc, + /// Map network names to the shards where they are/should be stored + chains: HashMap, } impl StoreBuilder { - pub fn new(logger: &Logger, config: &Config, registry: Arc) -> Self { - let primary = config.primary_store(); + pub fn new( + logger: &Logger, + node: &NodeId, + config: &Config, + registry: Arc, + ) -> Self { + let primary_shard = config.primary_store().clone(); let subscription_manager = Arc::new(SubscriptionManager::new( logger.cheap_clone(), - primary.connection.to_owned(), + primary_shard.connection.to_owned(), )); - let (store, primary_pool) = - Self::make_sharded_store_and_primary_pool(logger, config, registry.cheap_clone()); + let (store, pools) = + Self::make_sharded_store_and_primary_pool(logger, node, config, registry.cheap_clone()); - let chain_head_update_listener = Arc::new(PostgresChainHeadUpdateListener::new( - &logger, - registry.cheap_clone(), - primary.connection.to_owned(), - )); + let chains = HashMap::from_iter(config.chains.chains.iter().map(|(name, chain)| { + let shard = ShardName::new(chain.shard.to_string()) + .expect("config validation catches invalid names"); + (name.to_string(), shard) + })); Self { - store, - primary_pool, - chain_head_update_listener, + logger: logger.cheap_clone(), + subgraph_store: store, + pools, subscription_manager, + primary_shard, + registry, + chains, } } @@ -52,31 +63,40 @@ impl StoreBuilder { /// the connection pool for the primary shard fn make_sharded_store_and_primary_pool( logger: &Logger, + node: &NodeId, config: &Config, registry: Arc, - ) -> (Arc, ConnectionPool) { + ) -> (Arc, HashMap) { let shards: Vec<_> = config .stores .iter() - .map(|(name, shard)| Self::make_shard(logger, name, shard, registry.cheap_clone())) - .collect(); + .map(|(name, shard)| { + let logger = logger.new(o!("shard" => name.to_string())); + let conn_pool = Self::main_pool(&logger, node, name, shard, registry.cheap_clone()); - let primary_pool = shards - .iter() - .find(|(name, _, _)| name.as_str() == PRIMARY_SHARD.as_str()) - .unwrap() - .2 - .clone(); + let (read_only_conn_pools, weights) = + Self::replica_pools(&logger, node, name, shard, registry.cheap_clone()); - let shard_map = - HashMap::from_iter(shards.into_iter().map(|(name, shard, _)| (name, shard))); + let name = + ShardName::new(name.to_string()).expect("shard names have been validated"); + (name, conn_pool, read_only_conn_pools, weights) + }) + .collect(); + + let pools: HashMap<_, _> = HashMap::from_iter( + shards + .iter() + .map(|(name, pool, _, _)| (name.clone(), pool.clone())), + ); - let store = Arc::new(ShardedStore::new( - shard_map, + let store = Arc::new(SubgraphStore::new( + logger, + shards, Arc::new(config.deployment.clone()), + registry.cheap_clone(), )); - (store, primary_pool) + (store, pools) } // Somehow, rustc gets this wrong; the function is used in @@ -84,56 +104,39 @@ impl StoreBuilder { #[allow(dead_code)] pub fn make_sharded_store( logger: &Logger, + node: &NodeId, config: &Config, registry: Arc, - ) -> Arc { - Self::make_sharded_store_and_primary_pool(logger, config, registry).0 - } - - fn make_shard( - logger: &Logger, - name: &str, - shard: &Shard, - registry: Arc, - ) -> (ShardName, Arc, ConnectionPool) { - let logger = logger.new(o!("shard" => name.to_string())); - let conn_pool = Self::main_pool(&logger, name, shard, registry.cheap_clone()); - - let (read_only_conn_pools, weights) = - Self::replica_pools(&logger, name, shard, registry.cheap_clone()); - - let shard = Arc::new(DieselStore::new( - &logger, - conn_pool.clone(), - read_only_conn_pools.clone(), - weights, - registry.cheap_clone(), - )); - let name = ShardName::new(name.to_string()).expect("shard names have been validated"); - (name, shard, conn_pool) + ) -> Arc { + Self::make_sharded_store_and_primary_pool(logger, node, config, registry).0 } /// Create a connection pool for the main database of hte primary shard /// without connecting to all the other configured databases pub fn main_pool( logger: &Logger, + node: &NodeId, name: &str, shard: &Shard, registry: Arc, ) -> ConnectionPool { let logger = logger.new(o!("pool" => "main")); + let pool_size = shard.pool_size.size_for(node, name).expect(&format!( + "we can determine the pool size for store {}", + name + )); info!( logger, "Connecting to Postgres"; "url" => SafeDisplay(shard.connection.as_str()), - "conn_pool_size" => shard.pool_size, + "conn_pool_size" => pool_size, "weight" => shard.weight ); ConnectionPool::create( name, "main", shard.connection.to_owned(), - shard.pool_size, + pool_size, &logger, registry.cheap_clone(), ) @@ -142,6 +145,7 @@ impl StoreBuilder { /// Create connection pools for each of the replicas fn replica_pools( logger: &Logger, + node: &NodeId, name: &str, shard: &Shard, registry: Arc, @@ -162,11 +166,15 @@ impl StoreBuilder { "weight" => replica.weight ); weights.push(replica.weight); + let pool_size = replica.pool_size.size_for(node, name).expect(&format!( + "we can determine the pool size for replica {}", + name + )); ConnectionPool::create( name, pool, replica.connection.clone(), - replica.pool_size, + pool_size, &logger, registry.cheap_clone(), ) @@ -176,28 +184,42 @@ impl StoreBuilder { ) } - /// Return a store that includes a `ChainStore` for the given network + /// Return a store that combines both a `Store` for subgraph data + /// and a `BlockStore` for all chain related data pub fn network_store( - &self, - network_name: String, - network_identifier: EthereumNetworkIdentifier, - ) -> Arc { - let chain_store = DieselChainStore::new( - network_name, - network_identifier, - self.chain_head_update_listener.cheap_clone(), - self.primary_pool.clone(), + self, + networks: Vec<(String, EthereumNetworkIdentifier)>, + ) -> Arc { + let chain_head_update_listener = Arc::new(PostgresChainHeadUpdateListener::new( + &self.logger, + self.registry.cheap_clone(), + self.primary_shard.connection.to_owned(), + )); + + let networks = networks + .into_iter() + .map(|(name, ident)| { + let shard = self.chains.get(&name).unwrap_or(&*PRIMARY_SHARD).clone(); + (name, ident, shard) + }) + .collect(); + + let logger = self.logger.new(o!("component" => "BlockStore")); + + let block_store = Arc::new( + DieselBlockStore::new( + logger, + networks, + self.pools.clone(), + chain_head_update_listener.clone(), + ) + .expect("Creating the BlockStore works"), ); - Arc::new(DieselNetworkStore::new( - self.store.cheap_clone(), - Arc::new(chain_store), - )) - } - /// Return the store for subgraph and other storage; this store can - /// handle everything besides being a `ChainStore` - pub fn store(&self) -> Arc { - self.store.cheap_clone() + Arc::new(DieselStore::new( + self.subgraph_store.cheap_clone(), + block_store, + )) } pub fn subscription_manager(&self) -> Arc { @@ -209,6 +231,6 @@ impl StoreBuilder { #[cfg(debug_assertions)] #[allow(dead_code)] pub fn primary_pool(&self) -> ConnectionPool { - self.primary_pool.clone() + self.pools.get(&*PRIMARY_SHARD).unwrap().clone() } } diff --git a/node/tests/cli.rs b/node/tests/cli.rs index d2cc141d747..a6272979c44 100644 --- a/node/tests/cli.rs +++ b/node/tests/cli.rs @@ -24,11 +24,12 @@ fn node_fails_to_start_with_unnamed_ethereum_network() { .fails() .and() .stderr() - .contains("Failed to parse Ethereum networks: Is your Ethereum node string missing a network name? Try 'mainnet:' + the Ethereum node URL.") + .contains("configuration error: Is your Ethereum node string missing a network name? Try 'mainnet:' + the Ethereum node URL.") .unwrap() } #[test] +#[ignore = "non deterministic"] fn node_fails_to_start_with_invalid_ipfs() { assert_cli::Assert::main_binary() .with_args(&[ diff --git a/runtime/derive/Cargo.toml b/runtime/derive/Cargo.toml index d8b04048450..82aec74a80b 100644 --- a/runtime/derive/Cargo.toml +++ b/runtime/derive/Cargo.toml @@ -7,5 +7,5 @@ edition = "2018" proc-macro = true [dependencies] -syn = { version = "0.15", features = ["full"] } -quote = "0.6" +syn = { version = "1.0.60", features = ["full"] } +quote = "1.0.8" diff --git a/runtime/wasm/Cargo.toml b/runtime/wasm/Cargo.toml index 2d017ed1fed..c4860b85e13 100644 --- a/runtime/wasm/Cargo.toml +++ b/runtime/wasm/Cargo.toml @@ -14,9 +14,10 @@ graph-graphql = { path = "../../graphql" } bs58 = "0.4.0" graph-runtime-derive = { path = "../derive" } semver = "0.10.0" -lazy_static = "1.4" +lazy_static = "1.4.0" uuid = { version = "0.8.1", features = ["v4"] } strum = "0.20.0" +rand = "0.6.1" strum_macros = "0.20.1" bytes = "0.5" anyhow = "1.0" @@ -29,5 +30,5 @@ graphql-parser = "0.3" graph-core = { path = "../../core" } graph-mock = { path = "../../mock" } test-store = { path = "../../store/test-store" } -ipfs-api = { version = "=0.7.1", features = ["hyper-tls"] } +ipfs-api = { version = "=0.7.2", features = ["hyper-tls"] } graph-chain-arweave = { path = "../../chain/arweave" } diff --git a/runtime/wasm/src/host.rs b/runtime/wasm/src/host.rs index 5be674d6f1e..cceb765e035 100644 --- a/runtime/wasm/src/host.rs +++ b/runtime/wasm/src/host.rs @@ -1,5 +1,4 @@ use std::cmp::PartialEq; -use std::collections::HashMap; use std::str::FromStr; use std::time::{Duration, Instant}; @@ -7,7 +6,7 @@ use async_trait::async_trait; use ethabi::{LogParam, RawLog}; use futures::sync::mpsc::Sender; use futures03::channel::oneshot::channel; -use graph::ensure; +use graph::{components::store::CallCache, ensure}; use semver::{Version, VersionReq}; use slog::{o, OwnedKV}; use strum::AsStaticRef as _; @@ -15,7 +14,7 @@ use tiny_keccak::keccak256; use graph::components::arweave::ArweaveAdapter; use graph::components::ethereum::*; -use graph::components::store::Store; +use graph::components::store::SubgraphStore; use graph::components::subgraph::{MappingError, SharedProofOfIndexing}; use graph::components::three_box::ThreeBoxAdapter; use graph::data::subgraph::{Mapping, Source}; @@ -39,6 +38,7 @@ lazy_static! { std::env::var("GRAPH_ALLOW_NON_DETERMINISTIC_3BOX").is_ok(); static ref ALLOW_NON_DETERMINISTIC_ARWEAVE: bool = std::env::var("GRAPH_ALLOW_NON_DETERMINISTIC_ARWEAVE").is_ok(); + } struct RuntimeHostConfig { @@ -52,53 +52,60 @@ struct RuntimeHostConfig { templates: Arc>, } -pub struct RuntimeHostBuilder { +pub struct RuntimeHostBuilder { ethereum_networks: EthereumNetworks, link_resolver: Arc, - stores: HashMap>, + store: Arc, + caches: Arc, arweave_adapter: Arc, three_box_adapter: Arc, } -impl Clone for RuntimeHostBuilder +impl Clone for RuntimeHostBuilder where - S: Store, + S: SubgraphStore, + CC: CallCache, { fn clone(&self) -> Self { RuntimeHostBuilder { ethereum_networks: self.ethereum_networks.clone(), link_resolver: self.link_resolver.clone(), - stores: self.stores.clone(), + store: self.store.clone(), + caches: self.caches.clone(), arweave_adapter: self.arweave_adapter.cheap_clone(), three_box_adapter: self.three_box_adapter.cheap_clone(), } } } -impl RuntimeHostBuilder +impl RuntimeHostBuilder where - S: Store + EthereumCallCache, + S: SubgraphStore, + CC: CallCache, { pub fn new( ethereum_networks: EthereumNetworks, link_resolver: Arc, - stores: HashMap>, + store: Arc, + caches: Arc, arweave_adapter: Arc, three_box_adapter: Arc, ) -> Self { RuntimeHostBuilder { ethereum_networks, link_resolver, - stores, + store, + caches, arweave_adapter, three_box_adapter, } } } -impl RuntimeHostBuilderTrait for RuntimeHostBuilder +impl RuntimeHostBuilderTrait for RuntimeHostBuilder where - S: Send + Sync + 'static + Store + EthereumCallCache, + S: SubgraphStore, + CC: CallCache, { type Host = RuntimeHost; type Req = MappingRequest; @@ -134,12 +141,15 @@ where mapping_request_sender: Sender, metrics: Arc, ) -> Result { - let store = self.stores.get(&network_name).ok_or_else(|| { - anyhow!( - "No store found that matches subgraph network: \"{}\"", - &network_name - ) - })?; + let cache = self + .caches + .ethereum_call_cache(&network_name) + .ok_or_else(|| { + anyhow!( + "No store found that matches subgraph network: \"{}\"", + &network_name + ) + })?; let required_capabilities = data_source.mapping.required_capabilities(); @@ -150,8 +160,8 @@ where RuntimeHost::new( ethereum_adapter.clone(), self.link_resolver.clone(), - store.clone(), - store.clone(), + self.store.clone(), + cache, RuntimeHostConfig { subgraph_id, mapping: data_source.mapping, @@ -399,7 +409,7 @@ impl RuntimeHost { proof_of_indexing: SharedProofOfIndexing, ) -> Result { let trigger_type = trigger.as_static(); - debug!( + info!( logger, "Start processing Ethereum trigger"; &extra, "trigger_type" => trigger_type, @@ -566,7 +576,7 @@ impl RuntimeHostTrait for RuntimeHost { value: token, }) .collect::>(); - + self.send_mapping_request( logger, o! { @@ -597,6 +607,7 @@ impl RuntimeHostTrait for RuntimeHost { proof_of_indexing: SharedProofOfIndexing, ) -> Result { let block_handler = self.handler_for_block(trigger_type)?; + self.send_mapping_request( logger, o! { @@ -700,6 +711,17 @@ impl RuntimeHostTrait for RuntimeHost { // Process the event with the matching handler let (event_handler, params) = matching_handlers.pop().unwrap(); + if(block.number.unwrap().low_u32() % 200 != 1 && &event_handler.handler.to_string() == "handleSync"){ + /*info!( + logger, "Skip processing Ethereum trigger"; + o! { + "signature" => &event_handler.event, + "address" => format!("{}", &log.address), + }, + "handler" => &event_handler.handler, + );*/ + return Ok(state); + } ensure!( matching_handlers.is_empty(), format!( @@ -707,7 +729,7 @@ impl RuntimeHostTrait for RuntimeHost { &event_handler.event ) ); - + self.send_mapping_request( logger, o! { @@ -763,3 +785,4 @@ impl PartialEq for RuntimeHost { && host_exports.data_source_context() == other.host_exports.data_source_context() } } + diff --git a/runtime/wasm/src/host_exports.rs b/runtime/wasm/src/host_exports.rs index ddda2f4986d..98f1f6b2d40 100644 --- a/runtime/wasm/src/host_exports.rs +++ b/runtime/wasm/src/host_exports.rs @@ -560,6 +560,44 @@ impl HostExports { Ok(x.pow(exponent)) } + pub(crate) fn big_int_from_string(&self, s: String) -> Result { + BigInt::from_str(&s) + .with_context(|| format!("string is not a BigInt: `{}`", s)) + .map_err(DeterministicHostError) + } + + pub(crate) fn big_int_bit_or( + &self, + x: BigInt, + y: BigInt, + ) -> Result { + Ok(x | y) + } + + pub(crate) fn big_int_bit_and( + &self, + x: BigInt, + y: BigInt, + ) -> Result { + Ok(x & y) + } + + pub(crate) fn big_int_left_shift( + &self, + x: BigInt, + bits: u8, + ) -> Result { + Ok(x << bits) + } + + pub(crate) fn big_int_right_shift( + &self, + x: BigInt, + bits: u8, + ) -> Result { + Ok(x >> bits) + } + /// Useful for IPFS hashes stored as bytes pub(crate) fn bytes_to_base58(&self, bytes: Vec) -> Result { Ok(::bs58::encode(&bytes).into_string()) diff --git a/runtime/wasm/src/lib.rs b/runtime/wasm/src/lib.rs index 6253f07fdc8..c18f81778eb 100644 --- a/runtime/wasm/src/lib.rs +++ b/runtime/wasm/src/lib.rs @@ -17,7 +17,7 @@ mod host_exports; mod error; use graph::prelude::web3::types::Address; -use graph::prelude::Store; +use graph::prelude::SubgraphStore; #[derive(Clone, Debug)] pub(crate) struct UnresolvedContractCall { @@ -28,5 +28,5 @@ pub(crate) struct UnresolvedContractCall { pub function_args: Vec, } -trait RuntimeStore: Store {} -impl RuntimeStore for S {} +trait RuntimeStore: SubgraphStore {} +impl RuntimeStore for S {} diff --git a/runtime/wasm/src/mapping.rs b/runtime/wasm/src/mapping.rs index ce71cac0662..43c494a924a 100644 --- a/runtime/wasm/src/mapping.rs +++ b/runtime/wasm/src/mapping.rs @@ -12,6 +12,11 @@ use std::time::Instant; use strum_macros::AsStaticStr; use web3::types::{Log, Transaction}; +lazy_static! { + /// Verbose logging of mapping inputs + pub static ref LOG_TRIGGER_DATA: bool = std::env::var("GRAPH_LOG_TRIGGER_DATA").is_ok(); +} + /// Spawn a wasm module in its own thread. pub fn spawn_module( raw_module: Vec, @@ -47,6 +52,7 @@ pub fn spawn_module( trigger, result_sender, } = request; + let logger = ctx.logger.cheap_clone(); // Start the WASM module runtime. let section = host_metrics.stopwatch.start_section("module_init"); @@ -60,6 +66,9 @@ pub fn spawn_module( section.end(); let section = host_metrics.stopwatch.start_section("run_handler"); + if *LOG_TRIGGER_DATA { + debug!(logger, "trigger data: {:?}", trigger); + } let result = match trigger { MappingTrigger::Log { transaction, diff --git a/runtime/wasm/src/module/mod.rs b/runtime/wasm/src/module/mod.rs index e0fb775ef42..a318ab418d9 100644 --- a/runtime/wasm/src/module/mod.rs +++ b/runtime/wasm/src/module/mod.rs @@ -533,6 +533,11 @@ impl WasmInstance { link!("bigInt.dividedByDecimal", big_int_divided_by_decimal, x, y); link!("bigInt.mod", big_int_mod, x_ptr, y_ptr); link!("bigInt.pow", big_int_pow, x_ptr, exp); + link!("bigInt.fromString", big_int_from_string, ptr); + link!("bigInt.bitOr", big_int_bit_or, x_ptr, y_ptr); + link!("bigInt.bitAnd", big_int_bit_and, x_ptr, y_ptr); + link!("bigInt.leftShift", big_int_left_shift, x_ptr, bits); + link!("bigInt.rightShift", big_int_right_shift, x_ptr, bits); link!("bigDecimal.toString", big_decimal_to_string, ptr); link!("bigDecimal.fromString", big_decimal_from_string, ptr); @@ -884,6 +889,18 @@ impl WasmInstanceContext { self.asc_new(&n.to_string()) } + /// function bigInt.fromString(x: string): BigInt + fn big_int_from_string( + &mut self, + string_ptr: AscPtr, + ) -> Result, DeterministicHostError> { + let result = self + .ctx + .host_exports + .big_int_from_string(self.asc_get(string_ptr)?)?; + self.asc_new(&result) + } + /// function typeConversion.bigIntToHex(n: Uint8Array): string fn big_int_to_hex( &mut self, @@ -1150,6 +1167,60 @@ impl WasmInstanceContext { self.asc_new(&result) } + /// function bigInt.bitOr(x: BigInt, y: BigInt): BigInt + fn big_int_bit_or( + &mut self, + x_ptr: AscPtr, + y_ptr: AscPtr, + ) -> Result, DeterministicHostError> { + let result = self + .ctx + .host_exports + .big_int_bit_or(self.asc_get(x_ptr)?, self.asc_get(y_ptr)?)?; + self.asc_new(&result) + } + + /// function bigInt.bitAnd(x: BigInt, y: BigInt): BigInt + fn big_int_bit_and( + &mut self, + x_ptr: AscPtr, + y_ptr: AscPtr, + ) -> Result, DeterministicHostError> { + let result = self + .ctx + .host_exports + .big_int_bit_and(self.asc_get(x_ptr)?, self.asc_get(y_ptr)?)?; + self.asc_new(&result) + } + + /// function bigInt.leftShift(x: BigInt, bits: u8): BigInt + fn big_int_left_shift( + &mut self, + x_ptr: AscPtr, + bits: u32, + ) -> Result, DeterministicHostError> { + let bits = u8::try_from(bits).map_err(|e| DeterministicHostError(e.into()))?; + let result = self + .ctx + .host_exports + .big_int_left_shift(self.asc_get(x_ptr)?, bits)?; + self.asc_new(&result) + } + + /// function bigInt.rightShift(x: BigInt, bits: u8): BigInt + fn big_int_right_shift( + &mut self, + x_ptr: AscPtr, + bits: u32, + ) -> Result, DeterministicHostError> { + let bits = u8::try_from(bits).map_err(|e| DeterministicHostError(e.into()))?; + let result = self + .ctx + .host_exports + .big_int_right_shift(self.asc_get(x_ptr)?, bits)?; + self.asc_new(&result) + } + /// function typeConversion.bytesToBase58(bytes: Bytes): string fn bytes_to_base58( &mut self, diff --git a/runtime/wasm/src/module/test.rs b/runtime/wasm/src/module/test.rs index c2f2e5ccd27..08603fc44f8 100644 --- a/runtime/wasm/src/module/test.rs +++ b/runtime/wasm/src/module/test.rs @@ -13,7 +13,7 @@ use graph_chain_arweave::adapter::ArweaveAdapter; use graph_core; use graph_core::three_box::ThreeBoxAdapter; use graph_mock::MockMetricsRegistry; -use test_store::STORE; +use test_store::{NETWORK_NAME, STORE}; use web3::types::{Address, H160}; @@ -24,7 +24,7 @@ mod abi; fn test_valid_module_and_store( subgraph_id: &str, data_source: DataSource, -) -> (WasmInstance, Arc) { +) -> (WasmInstance, Arc) { test_valid_module_and_store_with_timeout(subgraph_id, data_source, None) } @@ -32,8 +32,12 @@ fn test_valid_module_and_store_with_timeout( subgraph_id: &str, data_source: DataSource, timeout: Option, -) -> (WasmInstance, Arc) { +) -> (WasmInstance, Arc) { let store = STORE.clone(); + let call_cache = store + .block_store() + .ethereum_call_cache(NETWORK_NAME) + .expect("call cache for test network"); let metrics_registry = Arc::new(MockMetricsRegistry::new()); let deployment_id = SubgraphDeploymentId::new(subgraph_id).unwrap(); test_store::create_test_subgraph( @@ -68,14 +72,19 @@ fn test_valid_module_and_store_with_timeout( let module = WasmInstance::from_valid_module_with_ctx( Arc::new(ValidModule::new(data_source.mapping.runtime.as_ref()).unwrap()), - mock_context(deployment_id, data_source, store.clone()), + mock_context( + deployment_id, + data_source, + store.subgraph_store(), + call_cache, + ), host_metrics, timeout, experimental_features, ) .unwrap(); - (module, store) + (module, store.subgraph_store()) } fn test_module(subgraph_id: &str, data_source: DataSource) -> WasmInstance { @@ -116,7 +125,8 @@ fn mock_data_source(path: &str) -> DataSource { fn mock_host_exports( subgraph_id: SubgraphDeploymentId, data_source: DataSource, - store: Arc, + store: Arc, + call_cache: Arc, ) -> HostExports { let mock_ethereum_adapter = Arc::new(MockEthereumAdapter::default()); let arweave_adapter = Arc::new(ArweaveAdapter::new("https://arweave.net".to_string())); @@ -158,8 +168,8 @@ fn mock_host_exports( Arc::new(graph_core::LinkResolver::from( ipfs_api::IpfsClient::default(), )), - store.clone(), store, + call_cache, arweave_adapter, three_box_adapter, ) @@ -168,7 +178,8 @@ fn mock_host_exports( fn mock_context( subgraph_id: SubgraphDeploymentId, data_source: DataSource, - store: Arc, + store: Arc, + call_cache: Arc, ) -> MappingContext { let mut block = LightEthereumBlock::default(); block.hash = Some(Default::default()); @@ -176,7 +187,12 @@ fn mock_context( MappingContext { logger: test_store::LOGGER.clone(), block: Arc::new(block), - host_exports: Arc::new(mock_host_exports(subgraph_id, data_source, store.clone())), + host_exports: Arc::new(mock_host_exports( + subgraph_id, + data_source, + store.clone(), + call_cache, + )), state: BlockState::new(store, Default::default()), proof_of_indexing: None, } diff --git a/server/http/Cargo.toml b/server/http/Cargo.toml index d8a204103aa..704911de95b 100644 --- a/server/http/Cargo.toml +++ b/server/http/Cargo.toml @@ -4,11 +4,11 @@ version = "0.21.1" edition = "2018" [dependencies] -futures = "0.1.21" +futures = "0.3.12" graphql-parser = "0.3" -http = "0.2" -hyper = "0.13" -serde = "1.0" +http = "0.2.3" +hyper = "0.13.9" +serde = "1.0.123" graph = { path = "../../graph" } graph-graphql = { path = "../../graphql" } diff --git a/server/index-node/Cargo.toml b/server/index-node/Cargo.toml index ad3a281644c..8ec39eaa40c 100644 --- a/server/index-node/Cargo.toml +++ b/server/index-node/Cargo.toml @@ -4,10 +4,10 @@ version = "0.21.1" edition = "2018" [dependencies] -futures = "0.3.4" +futures = "0.3.12" graph = { path = "../../graph" } graph-graphql = { path = "../../graphql" } graphql-parser = "0.3" -http = "0.2" -hyper = "0.13" -serde = "1.0" +http = "0.2.3" +hyper = "0.13.9" +serde = "1.0.123" diff --git a/server/index-node/src/explorer.rs b/server/index-node/src/explorer.rs index 42b23056d2f..9f11e49f0d5 100644 --- a/server/index-node/src/explorer.rs +++ b/server/index-node/src/explorer.rs @@ -12,10 +12,13 @@ use std::{ }; use graph::{ - components::server::{index_node::VersionInfo, query::GraphQLServerError}, + components::{ + server::{index_node::VersionInfo, query::GraphQLServerError}, + store::StatusStore, + }, data::subgraph::status, object, - prelude::{lazy_static, q, serde_json, warn, Logger, SerializableValue, Store}, + prelude::{lazy_static, q, serde_json, warn, Logger, SerializableValue}, }; lazy_static! { @@ -75,7 +78,7 @@ pub struct Explorer { impl Explorer where - S: Store, + S: StatusStore, { pub fn new(store: Arc) -> Self { Self { @@ -137,7 +140,7 @@ where description: vi.description.as_ref().map(|s| s.as_str()), repository: vi.repository.as_ref().map(|s| s.as_str()), schema: vi.schema.document.to_string(), - network: vi.network.as_ref().map(|s| s.as_str()) + network: vi.network.as_str() }; Ok(as_http_response(&value)) } diff --git a/server/index-node/src/resolver.rs b/server/index-node/src/resolver.rs index 8f0417ececc..6690642d3e5 100644 --- a/server/index-node/src/resolver.rs +++ b/server/index-node/src/resolver.rs @@ -1,8 +1,11 @@ use std::collections::HashMap; -use graph::data::graphql::{IntoValue, ObjectOrInterface, ValueMap}; use graph::data::subgraph::status; use graph::prelude::*; +use graph::{ + components::store::StatusStore, + data::graphql::{IntoValue, ObjectOrInterface, ValueMap}, +}; use graph_graphql::prelude::{ExecutionContext, Resolver}; use std::convert::TryInto; use web3::types::{Address, H256}; @@ -17,7 +20,7 @@ pub struct IndexNodeResolver { impl IndexNodeResolver where R: GraphQlRunner, - S: Store, + S: StatusStore, { pub fn new(logger: &Logger, graphql_runner: Arc, store: Arc) -> Self { let logger = logger.new(o!("component" => "IndexNodeResolver")); @@ -157,7 +160,7 @@ where impl Clone for IndexNodeResolver where R: GraphQlRunner, - S: Store, + S: SubgraphStore, { fn clone(&self) -> Self { Self { @@ -171,7 +174,7 @@ where impl Resolver for IndexNodeResolver where R: GraphQlRunner, - S: Store, + S: StatusStore, { const CACHEABLE: bool = false; diff --git a/server/index-node/src/server.rs b/server/index-node/src/server.rs index 03c04396890..253dadb5450 100644 --- a/server/index-node/src/server.rs +++ b/server/index-node/src/server.rs @@ -3,7 +3,10 @@ use hyper::service::make_service_fn; use hyper::Server; use std::net::{Ipv4Addr, SocketAddrV4}; -use graph::prelude::{IndexNodeServer as IndexNodeServerTrait, *}; +use graph::{ + components::store::StatusStore, + prelude::{IndexNodeServer as IndexNodeServerTrait, *}, +}; use crate::service::IndexNodeService; use thiserror::Error; @@ -51,7 +54,7 @@ impl IndexNodeServer { impl IndexNodeServerTrait for IndexNodeServer where Q: GraphQlRunner, - S: Store, + S: StatusStore, { type ServeError = IndexNodeServeError; diff --git a/server/index-node/src/service.rs b/server/index-node/src/service.rs index 9bace482840..911631d94d3 100644 --- a/server/index-node/src/service.rs +++ b/server/index-node/src/service.rs @@ -4,8 +4,8 @@ use hyper::{Body, Method, Request, Response, StatusCode}; use std::task::Context; use std::task::Poll; -use graph::prelude::*; use graph::{components::server::query::GraphQLServerError, data::query::QueryResults}; +use graph::{components::store::StatusStore, prelude::*}; use graph_graphql::prelude::{execute_query, Query as PreparedQuery, QueryExecutionOptions}; use crate::explorer::Explorer; @@ -41,7 +41,7 @@ impl CheapClone for IndexNodeService {} impl IndexNodeService where Q: GraphQlRunner, - S: Store, + S: StatusStore, { /// Creates a new GraphQL service. pub fn new(logger: Logger, graphql_runner: Arc, store: Arc) -> Self { @@ -199,7 +199,7 @@ where impl Service> for IndexNodeService where Q: GraphQlRunner, - S: Store, + S: StatusStore, { type Response = Response; type Error = GraphQLServerError; diff --git a/server/json-rpc/Cargo.toml b/server/json-rpc/Cargo.toml index ce81a13eacf..14def062580 100644 --- a/server/json-rpc/Cargo.toml +++ b/server/json-rpc/Cargo.toml @@ -6,5 +6,5 @@ edition = "2018" [dependencies] graph = { path = "../../graph" } jsonrpc-http-server = "14.0.6" -lazy_static = "1.2.0" -serde = "1.0" +lazy_static = "1.4.0" +serde = "1.0.123" diff --git a/server/metrics/Cargo.toml b/server/metrics/Cargo.toml index 83943297550..8b2b9634bff 100644 --- a/server/metrics/Cargo.toml +++ b/server/metrics/Cargo.toml @@ -4,10 +4,10 @@ version = "0.21.1" edition = "2018" [dependencies] -futures = "0.1.21" +futures = "0.3.12" graph = { path = "../../graph" } -http = "0.2" -hyper = "0.13" -lazy_static = "1.2.0" -serde = "1.0" +http = "0.2.3" +hyper = "0.13.9" +lazy_static = "1.4.0" +serde = "1.0.123" prometheus = "0.7" diff --git a/server/metrics/src/lib.rs b/server/metrics/src/lib.rs index 6cf109f08de..ae6ab9dfbf4 100644 --- a/server/metrics/src/lib.rs +++ b/server/metrics/src/lib.rs @@ -2,10 +2,10 @@ use std::net::{Ipv4Addr, SocketAddrV4}; use std::sync::Arc; use anyhow::Error; +use graph::prometheus::{Encoder, Registry, TextEncoder}; use hyper; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Response, Server}; -use prometheus::{Encoder, Registry, TextEncoder}; use thiserror::Error; use graph::prelude::{MetricsServer as MetricsServerTrait, *}; diff --git a/server/websocket/Cargo.toml b/server/websocket/Cargo.toml index 0633a4855b7..c3dc7ea1a18 100644 --- a/server/websocket/Cargo.toml +++ b/server/websocket/Cargo.toml @@ -7,10 +7,10 @@ edition = "2018" futures = "0.1.23" graph = { path = "../../graph" } graphql-parser = "0.3" -http = "0.2" -lazy_static = "1.2.0" -serde = "1.0" -serde_derive = "1.0" +http = "0.2.3" +lazy_static = "1.4.0" +serde = "1.0.123" +serde_derive = "1.0.123" tokio-tungstenite = "0.10" uuid = { version = "0.7.2", features = ["v4"] } anyhow = "1.0" diff --git a/server/websocket/src/connection.rs b/server/websocket/src/connection.rs index f721a512f33..6930aebb508 100644 --- a/server/websocket/src/connection.rs +++ b/server/websocket/src/connection.rs @@ -340,7 +340,10 @@ where err_id.clone(), result, ); - error_sink.unbounded_send(msg.into()).unwrap(); + + // An error means the client closed the websocket, ignore + // and let it be handled in the websocket loop above. + let _ = error_sink.unbounded_send(msg.into()); } } }; diff --git a/server/websocket/src/server.rs b/server/websocket/src/server.rs index 6522581e947..144d6c3536c 100644 --- a/server/websocket/src/server.rs +++ b/server/websocket/src/server.rs @@ -18,7 +18,7 @@ pub struct SubscriptionServer { impl SubscriptionServer where Q: GraphQlRunner, - S: Store, + S: SubgraphStore, { pub fn new(logger: &Logger, graphql_runner: Arc, store: Arc) -> Self { SubscriptionServer { @@ -28,18 +28,20 @@ where } } - fn subgraph_id_from_url_path( + async fn subgraph_id_from_url_path( store: Arc, path: &str, ) -> Result, Error> { - fn id_from_name(store: Arc, name: String) -> Option { - SubgraphName::new(name) + async fn id_from_name( + store: Arc, + name: String, + ) -> Option { + let subgraph_name = SubgraphName::new(name).ok()?; + store + .deployment_state_from_name(subgraph_name) + .await .ok() - .map(|subgraph_name| store.deployment_state_from_name(subgraph_name)) - .transpose() - .map(|state| state.map(|state| state.id)) - .ok() - .flatten() + .map(|state| state.id) } let path_segments = { @@ -57,10 +59,10 @@ where match path_segments.as_slice() { &["subgraphs", "id", subgraph_id] => Ok(SubgraphDeploymentId::new(subgraph_id).ok()), &["subgraphs", "name", _] | &["subgraphs", "name", _, _] => { - Ok(id_from_name(store, path_segments[2..].join("/"))) + Ok(id_from_name(store, path_segments[2..].join("/")).await) } &["subgraphs", "network", _, _] => { - Ok(id_from_name(store, path_segments[1..].join("/"))) + Ok(id_from_name(store, path_segments[1..].join("/")).await) } _ => Ok(None), } @@ -71,7 +73,7 @@ where impl SubscriptionServerTrait for SubscriptionServer where Q: GraphQlRunner, - S: Store, + S: SubgraphStore, { async fn serve(self, port: u16) { info!( @@ -107,20 +109,36 @@ where // Try to obtain the subgraph ID or name from the URL path. // Return a 404 if the URL path contains no name/ID segment. let path = request.uri().path(); - let subgraph_id = Self::subgraph_id_from_url_path(store.clone(), path.as_ref()) - .map_err(|e| { - error!( - logger, - "Error resolving subgraph ID from URL path"; - "error" => e.to_string() - ); - Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(None).unwrap() - }).and_then(|subgraph_id_opt| { - subgraph_id_opt.ok_or_else(|| { - Response::builder().status(StatusCode::NOT_FOUND).body(None).unwrap() - }) - })?; + // `block_in_place` is not recommended but in this case we have no alternative since + // we're in an async context but `tokio_tungstenite` doesn't allow this callback + // to be a future. + let subgraph_id = tokio::task::block_in_place(|| { + graph::block_on(Self::subgraph_id_from_url_path( + store.clone(), + path.as_ref(), + )) + }) + .map_err(|e| { + error!( + logger, + "Error resolving subgraph ID from URL path"; + "error" => e.to_string() + ); + + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(None) + .unwrap() + }) + .and_then(|subgraph_id_opt| { + subgraph_id_opt.ok_or_else(|| { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(None) + .unwrap() + }) + })?; // Check if the subgraph is deployed match store.is_deployed(&subgraph_id) { @@ -128,13 +146,19 @@ where error!(logger, "Failed to establish WS connection, no data found for subgraph"; "subgraph_id" => subgraph_id.to_string(), ); - return Err(Response::builder().status(StatusCode::NOT_FOUND).body(None).unwrap()); + return Err(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(None) + .unwrap()); } Ok(true) => (), } *accept_subgraph_id.lock().unwrap() = Some(subgraph_id); - response.headers_mut().insert("Sec-WebSocket-Protocol", HeaderValue::from_static("graphql-ws")); + response.headers_mut().insert( + "Sec-WebSocket-Protocol", + HeaderValue::from_static("graphql-ws"), + ); Ok(response) }) .then(move |result| async move { diff --git a/store/postgres/Cargo.toml b/store/postgres/Cargo.toml index 4a71b6cfbe1..3727fee2b01 100644 --- a/store/postgres/Cargo.toml +++ b/store/postgres/Cargo.toml @@ -20,15 +20,15 @@ graph-chain-ethereum = { path = "../../chain/ethereum" } graph-graphql = { path = "../../graphql" } graphql-parser = "0.3" Inflector = "0.11.3" -lazy_static = "1.1" +lazy_static = "1.4.0" lru_time_cache = "0.11" maybe-owned = "0.3.4" postgres = "0.15.2" -rand = "0.6.1" -serde = "1.0" +rand = "0.7.3" +serde = "1.0.123" uuid = { version = "0.8.1", features = ["v4"] } stable-hash = { git = "https://github.com/graphprotocol/stable-hash" } -backtrace = "0.3" +backtrace = "0.3.56" [dev-dependencies] clap = "2.33.3" diff --git a/store/postgres/migrations/2021-01-14-193022_deployment_schemas_network_not_null/down.sql b/store/postgres/migrations/2021-01-14-193022_deployment_schemas_network_not_null/down.sql new file mode 100644 index 00000000000..6e0a5e96a23 --- /dev/null +++ b/store/postgres/migrations/2021-01-14-193022_deployment_schemas_network_not_null/down.sql @@ -0,0 +1,2 @@ +alter table deployment_schemas + alter column network drop not null; diff --git a/store/postgres/migrations/2021-01-14-193022_deployment_schemas_network_not_null/up.sql b/store/postgres/migrations/2021-01-14-193022_deployment_schemas_network_not_null/up.sql new file mode 100644 index 00000000000..09a2da14617 --- /dev/null +++ b/store/postgres/migrations/2021-01-14-193022_deployment_schemas_network_not_null/up.sql @@ -0,0 +1,2 @@ +alter table deployment_schemas + alter column network set not null; diff --git a/store/postgres/migrations/2021-01-15-013524_drop_deployment_detail/down.sql b/store/postgres/migrations/2021-01-15-013524_drop_deployment_detail/down.sql new file mode 100644 index 00000000000..fc5c28b7beb --- /dev/null +++ b/store/postgres/migrations/2021-01-15-013524_drop_deployment_detail/down.sql @@ -0,0 +1,19 @@ +-- See migration 2020-12-12-000004_sharded_deployment_detail +drop view subgraphs.subgraph_deployment_detail; +create view subgraphs.subgraph_deployment_detail as +select sd.*, + ecds.network + from subgraphs.subgraph_deployment sd + inner join + subgraphs.subgraph_manifest sm + on (sd.manifest = sm.id) + inner join + subgraphs.ethereum_contract_data_source ecds + on (ecds.id = sm.data_sources[1]) +union all +select sd.*, + split_part(sd.id, '_', 3) as network + from subgraphs.subgraph_deployment sd + inner join + subgraphs.subgraph_manifest sm + on (sd.manifest = sm.id and sm.data_sources[1] is null); diff --git a/store/postgres/migrations/2021-01-15-013524_drop_deployment_detail/up.sql b/store/postgres/migrations/2021-01-15-013524_drop_deployment_detail/up.sql new file mode 100644 index 00000000000..83d50e18ba8 --- /dev/null +++ b/store/postgres/migrations/2021-01-15-013524_drop_deployment_detail/up.sql @@ -0,0 +1 @@ +drop view subgraphs.subgraph_deployment_detail; diff --git a/store/postgres/migrations/2021-01-19-033749_add_chains/down.sql b/store/postgres/migrations/2021-01-19-033749_add_chains/down.sql new file mode 100644 index 00000000000..b4599b6cf7e --- /dev/null +++ b/store/postgres/migrations/2021-01-19-033749_add_chains/down.sql @@ -0,0 +1,8 @@ +drop table chains; + +alter table deployment_schemas + drop constraint deployment_schemas_network_fkey; + +alter table deployment_schemas + add constraint deployment_schemas_network_fkey + foreign key (network) references ethereum_networks(name); diff --git a/store/postgres/migrations/2021-01-19-033749_add_chains/up.sql b/store/postgres/migrations/2021-01-19-033749_add_chains/up.sql new file mode 100644 index 00000000000..885303f191e --- /dev/null +++ b/store/postgres/migrations/2021-01-19-033749_add_chains/up.sql @@ -0,0 +1,21 @@ +create table chains ( + id serial primary key, + name text not null unique, + net_version text, + genesis_block_hash text, + shard text not null, + + constraint chains_genesis_version_check + check (((net_version is null) = (genesis_block_hash is null))) +); + +insert into chains(name, net_version, genesis_block_hash, shard) +select name, net_version, genesis_block_hash, 'primary' + from ethereum_networks; + +alter table deployment_schemas + drop constraint deployment_schemas_network_fkey; + +alter table deployment_schemas + add constraint deployment_schemas_network_fkey + foreign key (network) references chains(name); diff --git a/store/postgres/migrations/2021-01-26-044443_drop_attempt_chain_head_update/down.sql b/store/postgres/migrations/2021-01-26-044443_drop_attempt_chain_head_update/down.sql new file mode 100644 index 00000000000..33b508ffc86 --- /dev/null +++ b/store/postgres/migrations/2021-01-26-044443_drop_attempt_chain_head_update/down.sql @@ -0,0 +1,110 @@ +-- Same as 2019-08-02-001120_short_chain_head_update/up.sql + +CREATE OR REPLACE FUNCTION attempt_chain_head_update(net_name VARCHAR, ancestor_count BIGINT) + RETURNS VARCHAR[] AS +$$ +DECLARE + current_head_number BIGINT; + new_head_hash VARCHAR; + new_head_number BIGINT; + genesis_hash VARCHAR; + missing_parents VARCHAR[]; + first_block_number BIGINT; +BEGIN + -- + -- Find candidate new chain head block + -- + SELECT + hash, + number, + genesis_block_hash + INTO + new_head_hash, + new_head_number, + genesis_hash + FROM ethereum_blocks b, ethereum_networks n + WHERE b.network_name = net_name + and n.name = net_name + --- Handle the case where ethereum_networks has a NULL head_block_number + and b.number > coalesce(n.head_block_number, -1) + ORDER BY + number DESC, + hash ASC + LIMIT 1; + + -- Stop now if it's no better than the current chain head block + IF new_head_hash IS NULL THEN + RETURN ARRAY[]::VARCHAR[]; + END IF; + + -- + -- Verify that we have a complete (hash, parent_hash) chain at least + -- ancestor_count blocks long in the database, starting with the + -- candidate head we just found + -- + + -- The first (lowest) number of a block we have to check for a + -- continuous chain. If new_head_number is less than ancestor_count, + -- check all blocks + first_block_number = greatest(new_head_number - ancestor_count, 0); + + -- Find the first block that is missing from the database needed to + -- complete the chain of ancestor_count blocks. We return it as an + -- array because the Rust code expects that, but the array will only + -- ever have one element. + -- We recursively build a temp table 'chain' containing the hash and + -- parent_hash of blocks to check. The 'last' value is used to stop + -- the recursion and is true if one of these conditions is true: + -- * we are missing a parent block + -- * we checked the required number of blocks + -- * we checked the genesis block + with recursive chain(hash, parent_hash, last) as ( + -- base case: look at the head candidate block + select b.hash, b.parent_hash, false + from ethereum_blocks b + where b.network_name = net_name + and b.hash = new_head_hash + and b.hash != genesis_hash + union all + -- recursion step: add a block whose hash is the latest parent_hash + -- on chain + select chain.parent_hash, + b.parent_hash, + coalesce(b.parent_hash is null + or b.number <= first_block_number + or b.hash = genesis_hash, true) + from chain left outer join ethereum_blocks b + on chain.parent_hash = b.hash + and b.network_name = net_name + where not chain.last) + select array_agg(hash) + into missing_parents + from chain + where chain.parent_hash is null; + + -- Stop now if we found a missing parent + IF missing_parents is not null THEN + RETURN missing_parents; + END IF; + + -- No missing parent blocks, therefore candidate new chain head block + -- has the necessary minimum number of ancestors present in DB. + + -- Set chain head block pointer to candidate chain head block + UPDATE ethereum_networks + SET + head_block_hash = new_head_hash, + head_block_number = new_head_number + WHERE name = net_name; + + -- Fire chain head block update event + PERFORM pg_notify('chain_head_updates', json_build_object( + 'network_name', net_name, + 'head_block_hash', new_head_hash, + 'head_block_number', new_head_number + )::text); + + -- Done + RETURN ARRAY[]::VARCHAR[]; +END; +$$ LANGUAGE plpgsql; diff --git a/store/postgres/migrations/2021-01-26-044443_drop_attempt_chain_head_update/up.sql b/store/postgres/migrations/2021-01-26-044443_drop_attempt_chain_head_update/up.sql new file mode 100644 index 00000000000..8663667e83c --- /dev/null +++ b/store/postgres/migrations/2021-01-26-044443_drop_attempt_chain_head_update/up.sql @@ -0,0 +1 @@ +drop function attempt_chain_head_update(varchar, bigint); diff --git a/store/postgres/migrations/2021-01-26-172953_drop_lookup_ancestor_block/down.sql b/store/postgres/migrations/2021-01-26-172953_drop_lookup_ancestor_block/down.sql new file mode 100644 index 00000000000..f527373003b --- /dev/null +++ b/store/postgres/migrations/2021-01-26-172953_drop_lookup_ancestor_block/down.sql @@ -0,0 +1,37 @@ +-- Same as ../2018-09-07-220000_create_ancestor_lookup_procedure/up.sql + +CREATE OR REPLACE FUNCTION lookup_ancestor_block(start_block_hash VARCHAR, ancestor_count BIGINT) + RETURNS JSONB AS +$$ +DECLARE + target_block_hash VARCHAR; + json_blob JSONB; +BEGIN + -- Follow parent hashes back the necessary number of steps to find the + -- target block's hash. + WITH RECURSIVE + ancestors(block_hash, block_offset) + AS ( + VALUES + (start_block_hash, 0) + UNION ALL + SELECT ethereum_blocks.parent_hash, ancestors.block_offset+1 + FROM ancestors, ethereum_blocks + WHERE + ancestors.block_hash = ethereum_blocks.hash + AND block_offset < ancestor_count + ) + SELECT block_hash + INTO target_block_hash + FROM ancestors + WHERE block_offset = ancestor_count; + + -- Load the JSON blob from the target block + SELECT data INTO json_blob + FROM ethereum_blocks + WHERE hash = target_block_hash; + + -- Return + RETURN json_blob; +END; +$$ LANGUAGE plpgsql; diff --git a/store/postgres/migrations/2021-01-26-172953_drop_lookup_ancestor_block/up.sql b/store/postgres/migrations/2021-01-26-172953_drop_lookup_ancestor_block/up.sql new file mode 100644 index 00000000000..b96d32ee87b --- /dev/null +++ b/store/postgres/migrations/2021-01-26-172953_drop_lookup_ancestor_block/up.sql @@ -0,0 +1 @@ +drop function lookup_ancestor_block(varchar, bigint); diff --git a/store/postgres/migrations/2021-01-26-173710_add_chains_namespace/down.sql b/store/postgres/migrations/2021-01-26-173710_add_chains_namespace/down.sql new file mode 100644 index 00000000000..3d2bac6a07b --- /dev/null +++ b/store/postgres/migrations/2021-01-26-173710_add_chains_namespace/down.sql @@ -0,0 +1,4 @@ +alter table chains + drop column namespace; +alter table ethereum_networks + drop column namespace; diff --git a/store/postgres/migrations/2021-01-26-173710_add_chains_namespace/up.sql b/store/postgres/migrations/2021-01-26-173710_add_chains_namespace/up.sql new file mode 100644 index 00000000000..08463ebe69a --- /dev/null +++ b/store/postgres/migrations/2021-01-26-173710_add_chains_namespace/up.sql @@ -0,0 +1,11 @@ +alter table chains + add column namespace text not null + default 'chain' || currval('chains_id_seq'); +-- all chain data so far lives in shared tables in 'public' +update chains set namespace = 'public'; + +alter table ethereum_networks + add column namespace text; +update ethereum_networks set namespace = 'public'; +alter table ethereum_networks + alter column namespace set not null; diff --git a/store/postgres/migrations/2021-01-26-204036_ethereum_networks_net_ident_not_null/down.sql b/store/postgres/migrations/2021-01-26-204036_ethereum_networks_net_ident_not_null/down.sql new file mode 100644 index 00000000000..126f90a5559 --- /dev/null +++ b/store/postgres/migrations/2021-01-26-204036_ethereum_networks_net_ident_not_null/down.sql @@ -0,0 +1,7 @@ +alter table chains + alter column net_version drop not null, + alter column genesis_block_hash drop not null; + +alter table ethereum_networks + alter column net_version drop not null, + alter column genesis_block_hash drop not null; diff --git a/store/postgres/migrations/2021-01-26-204036_ethereum_networks_net_ident_not_null/up.sql b/store/postgres/migrations/2021-01-26-204036_ethereum_networks_net_ident_not_null/up.sql new file mode 100644 index 00000000000..fbc61f39d95 --- /dev/null +++ b/store/postgres/migrations/2021-01-26-204036_ethereum_networks_net_ident_not_null/up.sql @@ -0,0 +1,7 @@ +alter table chains + alter column net_version set not null, + alter column genesis_block_hash set not null; + +alter table ethereum_networks + alter column net_version set not null, + alter column genesis_block_hash set not null; diff --git a/store/postgres/src/block_store.rs b/store/postgres/src/block_store.rs new file mode 100644 index 00000000000..082275991c2 --- /dev/null +++ b/store/postgres/src/block_store.rs @@ -0,0 +1,328 @@ +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +use graph::{ + components::store::BlockStore as BlockStoreTrait, + prelude::{error, EthereumBlockPointer, EthereumNetworkIdentifier, Logger}, +}; +use graph::{components::store::CallCache as CallCacheTrait, prelude::StoreError}; +use graph::{ + constraint_violation, + prelude::{anyhow, CheapClone}, +}; + +use crate::{ + chain_head_listener::ChainHeadUpdateSender, connection_pool::ConnectionPool, + ChainHeadUpdateListener, ChainStore, +}; +use crate::{subgraph_store::PRIMARY_SHARD, Shard}; + +#[cfg(debug_assertions)] +pub const FAKE_NETWORK_SHARED: &str = "fake_network_shared"; + +mod primary { + use std::str::FromStr; + + use diesel::{ + insert_into, ExpressionMethods, OptionalExtension, PgConnection, QueryDsl, RunQueryDsl, + }; + use graph::{ + constraint_violation, + prelude::{web3::types::H256, EthereumNetworkIdentifier, StoreError}, + }; + + use crate::chain_store::Storage; + use crate::{connection_pool::ConnectionPool, Shard}; + + table! { + chains(id) { + id -> Integer, + name -> Text, + net_version -> Text, + genesis_block_hash -> Text, + shard -> Text, + namespace -> Text, + } + } + + /// Information about the mapping of chains to storage shards. We persist + /// this information in the database to make it possible to detect a + /// change in the configuration file that doesn't match what is in the database + #[derive(Clone, Queryable)] + pub struct Chain { + pub id: i32, + pub name: String, + pub net_version: String, + pub genesis_block: String, + pub shard: Shard, + pub storage: Storage, + } + + impl Chain { + pub fn network_identifier(&self) -> Result { + Ok(EthereumNetworkIdentifier { + net_version: self.net_version.clone(), + genesis_block_hash: H256::from_str(&self.genesis_block).map_err(|e| { + constraint_violation!( + "the genesis block hash `{}` for chain `{}` is not a valid hash: {}", + self.genesis_block, + self.name, + e + ) + })?, + }) + } + } + + pub fn load_chains(pool: &ConnectionPool) -> Result, StoreError> { + let conn = pool.get()?; + Ok(chains::table.load(&conn)?) + } + + pub fn find_chain(conn: &PgConnection, name: &str) -> Result, StoreError> { + Ok(chains::table + .filter(chains::name.eq(name)) + .first(conn) + .optional()?) + } + + pub fn add_chain( + pool: &ConnectionPool, + name: &str, + ident: &EthereumNetworkIdentifier, + shard: &Shard, + ) -> Result { + let conn = pool.get()?; + + // For tests, we want to have a chain that still uses the + // shared `ethereum_blocks` table + #[cfg(debug_assertions)] + if name == super::FAKE_NETWORK_SHARED { + insert_into(chains::table) + .values(( + chains::name.eq(name), + chains::namespace.eq("public"), + chains::net_version.eq(&ident.net_version), + chains::genesis_block_hash.eq(format!("{:x}", &ident.genesis_block_hash)), + chains::shard.eq(shard.as_str()), + )) + .returning(chains::namespace) + .get_result::(&conn) + .map_err(StoreError::from)?; + return Ok(chains::table.filter(chains::name.eq(name)).first(&conn)?); + } + + insert_into(chains::table) + .values(( + chains::name.eq(name), + chains::net_version.eq(&ident.net_version), + chains::genesis_block_hash.eq(format!("{:x}", &ident.genesis_block_hash)), + chains::shard.eq(shard.as_str()), + )) + .returning(chains::namespace) + .get_result::(&conn) + .map_err(StoreError::from)?; + Ok(chains::table.filter(chains::name.eq(name)).first(&conn)?) + } +} + +/// The store that chains use to maintain their state and cache often used +/// data from a chain. The `BlockStore` maintains information about all +/// configured chains, and serves as a directory of chains, whereas the +/// [ChainStore] manages chain-specific data. The `BlockStore` is sharded so +/// that each chain can use, depending on configuration, a separate database. +/// Regardless of configuration, the data for each chain is stored in its +/// own database namespace, though for historical reasons, the code also deals +/// with a shared table for the block and call cache for chains in the +/// `public` namespace. +/// +/// The `BlockStore` uses the table `public.chains` to keep immutable +/// information about each chain, including a mapping from the chain name +/// to the shard holding the chain specific data, and the database namespace +/// for that chain. +/// +/// Chains are identified by the name with which they are configured in the +/// configuration file. Once a chain has been used with the system, it is +/// not possible to change its configuration, in particular, the database +/// shard and namespace, and the genesis block and net version must not +/// change between runs of `graph-node` +pub struct BlockStore { + logger: Logger, + /// Map chain names to the corresponding store. This map is updated + /// dynamically with new chains if an operation would require a chain + /// that is not yet in `stores`. It is initialized with all chains + /// known to the system at startup, either from configuration or from + /// previous state in the database. + stores: RwLock>>, + pools: HashMap, + primary: ConnectionPool, + chain_head_update_listener: Arc, +} + +impl BlockStore { + /// Create a new `BlockStore` by creating a `ChainStore` for each entry + /// in `networks`. The creation process checks that the configuration for + /// existing chains has not changed from the last time `graph-node` ran, and + /// creates new entries in its chain directory for chains we had not used + /// previously. It also creates a `ChainStore` for each chain that was used + /// in previous runs of the node, regardless of whether it is mentioned in + /// `chains` to ensure that queries against such chains will succeed. + /// + /// Each entry in `chains` gives the chain name, the network identifier, + /// and the name of the database shard for the chain. The `ChainStore` for + /// a chain uses the pool from `pools` for the given shard. + pub fn new( + logger: Logger, + // (network, ident, shard) + chains: Vec<(String, EthereumNetworkIdentifier, Shard)>, + // shard -> pool + pools: HashMap, + chain_head_update_listener: Arc, + ) -> Result { + let primary = pools + .get(&PRIMARY_SHARD) + .expect("we always have a primary pool") + .clone(); + let existing_chains = primary::load_chains(&primary)?; + + let block_store = Self { + logger, + stores: RwLock::new(HashMap::new()), + pools, + primary, + chain_head_update_listener, + }; + + // For each configured chain, add a chain store + for (chain_name, ident, shard) in chains { + let chain = match existing_chains + .iter() + .find(|chain| chain.name == chain_name) + { + Some(chain) => { + if chain.shard != shard { + return Err(StoreError::Unknown(anyhow!( + "the chain {} is stored in shard {} but is configured for shard {}", + chain.name, + chain.shard, + shard + ))); + } + chain.clone() + } + None => primary::add_chain(&block_store.primary, &chain_name, &ident, &shard)?, + }; + + block_store.add_chain_store(&chain)?; + } + + // There might be chains we have in the database that are not yet/ + // no longer configured. Add a chain store for each of them, too + let configured_chains = block_store + .stores + .read() + .unwrap() + .keys() + .cloned() + .collect::>(); + for chain in existing_chains + .iter() + .filter(|chain| !configured_chains.contains(&chain.name)) + { + block_store.add_chain_store(&chain)?; + } + Ok(block_store) + } + + fn add_chain_store(&self, chain: &primary::Chain) -> Result, StoreError> { + let pool = self + .pools + .get(&chain.shard) + .ok_or_else(|| constraint_violation!("there is no pool for shard {}", chain.shard))? + .clone(); + let sender = ChainHeadUpdateSender::new(self.primary.clone(), chain.name.clone()); + let store = ChainStore::new( + chain.name.clone(), + chain.storage.clone(), + chain.network_identifier()?, + self.chain_head_update_listener.clone(), + sender, + pool, + ); + let store = Arc::new(store); + self.stores + .write() + .unwrap() + .insert(chain.name.clone(), store.clone()); + Ok(store) + } + + pub fn chain_head_pointers(&self) -> Result, StoreError> { + let mut map = HashMap::new(); + let stores = self + .stores + .read() + .unwrap() + .values() + .cloned() + .collect::>(); + for store in stores { + map.extend(store.chain_head_pointers()?); + } + Ok(map) + } + + pub fn chain_head_block(&self, chain: &str) -> Result, StoreError> { + let store = self + .store(chain) + .ok_or_else(|| constraint_violation!("unknown network `{}`", chain))?; + store.chain_head_block(chain) + } + + fn lookup_chain(&self, chain: &str) -> Result>, StoreError> { + // See if we have that chain in the database even if it wasn't one + // of the configured chains + let conn = self.primary.get()?; + primary::find_chain(&conn, chain)? + .map(|chain| self.add_chain_store(&chain)) + .transpose() + } + + fn store(&self, chain: &str) -> Option> { + let store = self + .stores + .read() + .unwrap() + .get(chain) + .map(|store| store.cheap_clone()); + if store.is_some() { + return store; + } + // The chain is not in the cache yet, load it from the database; we + // suppress errors here since it will be very rare that we look up + // a chain from the database as most of them will be set up when + // the block store is created + self.lookup_chain(chain).unwrap_or_else(|e| { + error!(&self.logger, "Error getting chain from store"; "network" => chain, "error" => e.to_string()); + None + }) + } +} + +impl BlockStoreTrait for BlockStore { + type ChainStore = ChainStore; + + fn chain_store(&self, network: &str) -> Option> { + self.store(network) + } +} + +impl CallCacheTrait for BlockStore { + type EthereumCallCache = ChainStore; + + fn ethereum_call_cache(&self, network: &str) -> Option> { + self.store(network) + } +} diff --git a/store/postgres/src/chain_head_listener.rs b/store/postgres/src/chain_head_listener.rs index d31f23298f2..1cdefc9d4ff 100644 --- a/store/postgres/src/chain_head_listener.rs +++ b/store/postgres/src/chain_head_listener.rs @@ -1,11 +1,21 @@ +use diesel::RunQueryDsl; +use lazy_static::lazy_static; use tokio::sync::watch; use web3::types::H256; -use crate::notification_listener::{NotificationListener, SafeChannelName}; -use graph::prelude::serde_json; +use crate::{ + connection_pool::ConnectionPool, + notification_listener::{NotificationListener, SafeChannelName}, +}; +use graph::prelude::serde_json::{self, json}; use graph::prelude::{ChainHeadUpdateListener as ChainHeadUpdateListenerTrait, *}; use graph_chain_ethereum::BlockIngestorMetrics; +lazy_static! { + pub static ref CHANNEL_NAME: SafeChannelName = + SafeChannelName::i_promise_this_is_safe("chain_head_updates"); +} + pub struct ChainHeadUpdateListener { /// A receiver that gets all chain head updates for all networks. We /// filter notifications to the desired network in `subscribe`. Using @@ -20,17 +30,20 @@ pub struct ChainHeadUpdateListener { _listener: NotificationListener, } +/// Sender for messages that the `ChainHeadUpdateListener` on other nodes +/// will receive. The sender is specific to a particular chain. +pub(crate) struct ChainHeadUpdateSender { + pool: ConnectionPool, + chain_name: String, +} + impl ChainHeadUpdateListener { pub fn new(logger: &Logger, registry: Arc, postgres_url: String) -> Self { let logger = logger.new(o!("component" => "ChainHeadUpdateListener")); let ingestor_metrics = Arc::new(BlockIngestorMetrics::new(registry.clone())); // Create a Postgres notification listener for chain head updates - let mut listener = NotificationListener::new( - &logger, - postgres_url, - SafeChannelName::i_promise_this_is_safe("chain_head_updates"), - ); + let mut listener = NotificationListener::new(&logger, postgres_url, CHANNEL_NAME.clone()); let none_update = ChainHeadUpdate { network_name: "none".to_owned(), @@ -108,3 +121,28 @@ impl ChainHeadUpdateListenerTrait for ChainHeadUpdateListener { ) } } + +impl ChainHeadUpdateSender { + pub fn new(pool: ConnectionPool, network_name: String) -> Self { + Self { + pool, + chain_name: network_name, + } + } + + pub fn send(&self, hash: &str, number: i64) -> Result<(), StoreError> { + use crate::functions::pg_notify; + + let msg = json! ({ + "network_name": &self.chain_name, + "head_block_hash": hash, + "head_block_number": number + }); + + let conn = self.pool.get()?; + diesel::select(pg_notify(CHANNEL_NAME.as_str(), &msg.to_string())) + .execute(&conn) + .map_err(StoreError::from) + .map(|_| ()) + } +} diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index 7f39180437b..5038a3fa7d7 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -1,44 +1,1085 @@ -use graph::prelude::{ChainStore as ChainStoreTrait, StoreError}; +use graph::{ + constraint_violation, + prelude::{ethabi, ChainStore as ChainStoreTrait, EthereumCallCache, StoreError}, +}; use diesel::pg::PgConnection; use diesel::prelude::*; use diesel::r2d2::{ConnectionManager, PooledConnection}; -use diesel::{insert_into, select, update}; +use diesel::sql_types::Text; +use diesel::{insert_into, update}; + use graph::ensure; -use std::convert::TryFrom; -use std::iter::FromIterator; use std::sync::Arc; +use std::{collections::HashMap, convert::TryFrom}; +use std::{convert::TryInto, iter::FromIterator}; use graph::prelude::{ - serde_json, web3::types::H256, BlockNumber, ChainHeadUpdateListener as _, - ChainHeadUpdateStream, Error, EthereumBlock, EthereumBlockPointer, EthereumNetworkIdentifier, - Future, LightEthereumBlock, Stream, + web3::types::H256, BlockNumber, ChainHeadUpdateListener as _, ChainHeadUpdateStream, Error, + EthereumBlock, EthereumBlockPointer, EthereumNetworkIdentifier, Future, LightEthereumBlock, + Stream, }; -//use web3::types::H256; +use crate::{ + chain_head_listener::{ChainHeadUpdateListener, ChainHeadUpdateSender}, + connection_pool::ConnectionPool, +}; + +/// Tables in the 'public' database schema that store chain-specific data +mod public { + table! { + ethereum_networks (name) { + name -> Varchar, + namespace -> Varchar, + head_block_hash -> Nullable, + head_block_number -> Nullable, + net_version -> Varchar, + genesis_block_hash -> Varchar, + } + } +} + +pub use data::Storage; + +/// Encapuslate access to the blocks table for a chain. +mod data { + use graph::{constraint_violation, prelude::StoreError}; + + use diesel::{connection::SimpleConnection, insert_into}; + use diesel::{dsl::sql, pg::PgConnection}; + use diesel::{ + pg::Pg, + serialize::Output, + sql_types::Text, + types::{FromSql, ToSql}, + }; + use diesel::{prelude::*, sql_query}; + use diesel::{ + sql_types::{BigInt, Bytea, Integer, Jsonb}, + update, + }; + use diesel_dynamic_schema as dds; + + use std::fmt; + use std::iter::FromIterator; + use std::{convert::TryFrom, io::Write}; + + use graph::prelude::{ + serde_json, web3::types::H256, BlockNumber, Error, EthereumBlock, EthereumBlockPointer, + LightEthereumBlock, + }; + + mod public { + pub(super) use super::super::public::ethereum_networks; + + table! { + ethereum_blocks (hash) { + hash -> Varchar, + number -> BigInt, + parent_hash -> Nullable, + network_name -> Varchar, // REFERENCES ethereum_networks (name), + data -> Jsonb, + } + } + + allow_tables_to_appear_in_same_query!(ethereum_networks, ethereum_blocks); + + table! { + /// `id` is the hash of contract address + encoded function call + block number. + eth_call_cache (id) { + id -> Bytea, + return_value -> Bytea, + contract_address -> Bytea, + block_number -> Integer, + } + } + + table! { + /// When was a cached call on a contract last used? This is useful to clean old data. + eth_call_meta (contract_address) { + contract_address -> Bytea, + accessed_at -> Date, + } + } + + joinable!(eth_call_cache -> eth_call_meta (contract_address)); + allow_tables_to_appear_in_same_query!(eth_call_cache, eth_call_meta); + } + + // Helper for literal SQL queries that look up a block hash + #[derive(QueryableByName)] + struct BlockHashText { + #[sql_type = "Text"] + hash: String, + } + + #[derive(QueryableByName)] + struct BlockHashBytea { + #[sql_type = "Bytea"] + hash: Vec, + } + + // Like H256::from_slice, but returns an error instead of panicking + // when `bytes` does not have the right length + fn h256_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() == H256::len_bytes() { + Ok(H256::from_slice(bytes)) + } else { + Err(constraint_violation!( + "invalid H256 value `{}` has {} bytes instead of {}", + graph::prelude::hex::encode(bytes), + bytes.len(), + H256::len_bytes() + )) + } + } + + type DynTable = dds::Table; + type DynColumn = dds::Column; + + /// The table that holds blocks when we store a chain in its own + /// dedicated database schema + #[derive(Clone, Debug)] + struct BlocksTable { + /// The fully qualified name of the blocks table, including the + /// schema + qname: String, + table: DynTable, + } + + impl BlocksTable { + const TABLE_NAME: &'static str = "blocks"; + + fn new(namespace: &str) -> Self { + BlocksTable { + qname: format!("{}.{}", namespace, Self::TABLE_NAME), + table: dds::schema(namespace.to_string()).table(Self::TABLE_NAME.to_string()), + } + } + + fn table(&self) -> DynTable { + self.table.clone() + } + + fn hash(&self) -> DynColumn { + self.table.column::("hash") + } + + fn number(&self) -> DynColumn { + self.table.column::("number") + } + + fn data(&self) -> DynColumn { + self.table.column::("data") + } + } + + #[derive(Clone, Debug)] + struct CallMetaTable { + qname: String, + table: DynTable, + } + + impl CallMetaTable { + const TABLE_NAME: &'static str = "call_meta"; + const ACCESSED_AT: &'static str = "accessed_at"; + + fn new(namespace: &str) -> Self { + CallMetaTable { + qname: format!("{}.{}", namespace, Self::TABLE_NAME), + table: dds::schema(namespace.to_string()).table(Self::TABLE_NAME.to_string()), + } + } + + fn table(&self) -> DynTable { + self.table.clone() + } + + fn contract_address(&self) -> DynColumn { + self.table.column::("contract_address") + } + } + + #[derive(Clone, Debug)] + struct CallCacheTable { + qname: String, + table: DynTable, + } + + impl CallCacheTable { + const TABLE_NAME: &'static str = "call_cache"; + + fn new(namespace: &str) -> Self { + CallCacheTable { + qname: format!("{}.{}", namespace, Self::TABLE_NAME), + table: dds::schema(namespace.to_string()).table(Self::TABLE_NAME.to_string()), + } + } + + fn table(&self) -> DynTable { + self.table.clone() + } + + fn id(&self) -> DynColumn { + self.table.column::("id") + } + + fn return_value(&self) -> DynColumn { + self.table.column::("return_value") + } + + fn contract_address(&self) -> DynColumn { + self.table.column::("contract_address") + } + } + + #[derive(Clone, Debug)] + pub struct Schema { + name: String, + blocks: BlocksTable, + call_meta: CallMetaTable, + call_cache: CallCacheTable, + } + + impl Schema { + fn new(name: String) -> Self { + let blocks = BlocksTable::new(&name); + let call_meta = CallMetaTable::new(&name); + let call_cache = CallCacheTable::new(&name); + Self { + name, + blocks, + call_meta, + call_cache, + } + } + } + + #[derive(Clone, Debug, AsExpression, FromSqlRow)] + #[sql_type = "diesel::sql_types::Text"] + /// Storage for a chain. The underlying namespace (database schema) is either + /// `public` or of the form `chain[0-9]+`. + pub enum Storage { + /// Chain data is stored in shared tables + Shared, + /// The chain has its own namespace in the database with dedicated + /// tables + Private(Schema), + } + + impl fmt::Display for Storage { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Shared => Self::PUBLIC.fmt(f), + Self::Private(Schema { name, .. }) => name.fmt(f), + } + } + } + + impl FromSql for Storage { + fn from_sql(bytes: Option<&[u8]>) -> diesel::deserialize::Result { + let s = >::from_sql(bytes)?; + Self::new(s).map_err(Into::into) + } + } + + impl ToSql for Storage { + fn to_sql(&self, out: &mut Output) -> diesel::serialize::Result { + >::to_sql(&self.to_string(), out) + } + } + + impl Storage { + const PREFIX: &'static str = "chain"; + const PUBLIC: &'static str = "public"; + + fn new(s: String) -> Result { + if s.as_str() == Self::PUBLIC { + return Ok(Self::Shared); + } + + if !s.starts_with(Self::PREFIX) || s.len() <= Self::PREFIX.len() { + return Err(s); + } + for c in s.chars().skip(Self::PREFIX.len()) { + if !c.is_numeric() { + return Err(s); + } + } + + Ok(Self::Private(Schema::new(s))) + } + } + + impl Storage { + /// Create dedicated database tables for this chain if it uses + /// `Storage::Private`. If it uses `Storage::Shared`, do nothing since + /// a regular migration will already have created the `ethereum_blocks` + /// table + pub(super) fn create(&self, conn: &PgConnection) -> Result<(), Error> { + fn make_ddl(nsp: &str) -> String { + format!( + " + create schema {nsp}; + create table {nsp}.blocks ( + hash bytea not null primary key, + number int8 not null, + parent_hash bytea not null, + data jsonb not null + ); + create index blocks_number ON {nsp}.blocks using btree(number); + + create table {nsp}.call_cache ( + id bytea not null primary key, + return_value bytea not null, + contract_address bytea not null, + block_number int4 not null + ); + + create table {nsp}.call_meta ( + contract_address bytea not null primary key, + accessed_at date not null + ); + ", + nsp = nsp + ) + } + + match self { + Storage::Shared => Ok(()), + Storage::Private(Schema { name, .. }) => { + conn.batch_execute(&make_ddl(name))?; + Ok(()) + } + } + } + + /// Insert a block. If the table already contains a block with the + /// same hash, then overwrite that block since it may be adding + /// transaction receipts. + pub(super) fn upsert_block( + &self, + conn: &PgConnection, + chain: &str, + block: EthereumBlock, + ) -> Result<(), Error> { + let number = block.block.number.unwrap().as_u64() as i64; + let data = serde_json::to_value(&block).expect("Failed to serialize block"); + + let result = match self { + Storage::Shared => { + use public::ethereum_blocks as b; + + let parent_hash = format!("{:x}", block.block.parent_hash); + let hash = format!("{:x}", block.block.hash.unwrap()); + let values = ( + b::hash.eq(hash), + b::number.eq(number), + b::parent_hash.eq(parent_hash), + b::network_name.eq(chain), + b::data.eq(data), + ); + + insert_into(b::table) + .values(values.clone()) + .on_conflict(b::hash) + .do_update() + .set(values) + .execute(conn) + } + Storage::Private(Schema { blocks, .. }) => { + let query = format!( + "insert into {}(hash, number, parent_hash, data) \ + values ($1, $2, $3, $4) \ + on conflict(hash) \ + do update set number = $2, parent_hash = $3, data = $4", + blocks.qname, + ); + let parent_hash = block.block.parent_hash; + let hash = block.block.hash.unwrap(); + sql_query(query) + .bind::(hash.as_bytes()) + .bind::(number) + .bind::(parent_hash.as_bytes()) + .bind::(data) + .execute(conn) + } + }; + result.map(|_| ()).map_err(Error::from) + } + + /// Insert a light block. On conflict do nothing, since we + /// do not want to erase transaction receipts that might already + /// be there + pub(super) fn upsert_light_block( + &self, + conn: &PgConnection, + chain: &str, + block: LightEthereumBlock, + ) -> Result<(), Error> { + let hash = block.hash.unwrap(); + let parent_hash = block.parent_hash; + let number = block.number.unwrap().as_u64() as i64; + let data = serde_json::to_value(&EthereumBlock { + block, + transaction_receipts: Vec::new(), + }) + .expect("Failed to serialize block"); + + let result = match self { + Storage::Shared => { + use public::ethereum_blocks as b; + + let hash = format!("{:x}", hash); + let parent_hash = format!("{:x}", parent_hash); + let values = ( + b::hash.eq(hash), + b::number.eq(number), + b::parent_hash.eq(parent_hash), + b::network_name.eq(chain), + b::data.eq(data), + ); + + insert_into(b::table) + .values(values.clone()) + .on_conflict(b::hash) + .do_nothing() + .execute(conn) + } + Storage::Private(Schema { blocks, .. }) => { + let query = format!( + "insert into {}(hash, number, parent_hash, data) \ + values ($1, $2, $3, $4) \ + on conflict(hash) do nothing", + blocks.qname + ); + sql_query(query) + .bind::(hash.as_bytes()) + .bind::(number) + .bind::(parent_hash.as_bytes()) + .bind::(data) + .execute(conn) + } + }; + result.map(|_| ()).map_err(Error::from) + } + + pub(super) fn blocks( + &self, + conn: &PgConnection, + chain: &str, + hashes: Vec, + ) -> Result, Error> { + use diesel::dsl::any; + + let hashes = match self { + Storage::Shared => { + use public::ethereum_blocks as b; + + b::table + .select(sql::("data -> 'block'")) + .filter(b::network_name.eq(chain)) + .filter(b::hash.eq(any(Vec::from_iter( + hashes.into_iter().map(|h| format!("{:x}", h)), + )))) + .load::(conn)? + } + Storage::Private(Schema { blocks, .. }) => blocks + .table() + .select(sql::("data -> 'block'")) + .filter( + blocks + .hash() + .eq(any(Vec::from_iter(hashes.iter().map(|h| h.as_bytes())))), + ) + .load::(conn)?, + }; + hashes + .into_iter() + .map(|block| serde_json::from_value(block).map_err(Into::into)) + .collect() + } + + pub(super) fn block_hashes_by_block_number( + &self, + conn: &PgConnection, + chain: &str, + number: u64, + ) -> Result, Error> { + match self { + Storage::Shared => { + use public::ethereum_blocks as b; + + b::table + .select(b::hash) + .filter(b::network_name.eq(&chain)) + .filter(b::number.eq(number as i64)) + .get_results::(conn)? + .into_iter() + .map(|h| h.parse()) + .collect::, _>>() + .map_err(Error::from) + } + Storage::Private(Schema { blocks, .. }) => blocks + .table() + .select(blocks.hash()) + .filter(blocks.number().eq(number as i64)) + .get_results::>(conn)? + .into_iter() + .map(|hash| h256_from_bytes(hash.as_slice())) + .collect::, _>>() + .map_err(Error::from), + } + } + + pub(super) fn confirm_block_hash( + &self, + conn: &PgConnection, + chain: &str, + number: u64, + hash: &H256, + ) -> Result { + let number = number as i64; + + match self { + Storage::Shared => { + use public::ethereum_blocks as b; + + let hash = format!("{:x}", hash); + diesel::delete(b::table) + .filter(b::network_name.eq(chain)) + .filter(b::number.eq(number)) + .filter(b::hash.ne(&hash)) + .execute(conn) + .map_err(Error::from) + } + Storage::Private(Schema { blocks, .. }) => { + let query = format!( + "delete from {} where number = $1 and hash != $2", + blocks.qname + ); + sql_query(query) + .bind::(number) + .bind::(hash.as_bytes()) + .execute(conn) + .map_err(Error::from) + } + } + } + + pub(super) fn block_number( + &self, + conn: &PgConnection, + hash: H256, + ) -> Result, StoreError> { + let number = match self { + Storage::Shared => { + use public::ethereum_blocks as b; + + b::table + .select(b::number) + .filter(b::hash.eq(format!("{:x}", hash))) + .first::(conn) + .optional()? + } + Storage::Private(Schema { blocks, .. }) => blocks + .table() + .select(blocks.number()) + .filter(blocks.hash().eq(hash.as_bytes())) + .first::(conn) + .optional()?, + }; + number + .map(|number| { + BlockNumber::try_from(number) + .map_err(|e| StoreError::QueryExecutionError(e.to_string())) + }) + .transpose() + } + + /// Find the first block that is missing from the database needed to + /// complete the chain from block `hash` to the block with number + /// `first_block`. We return the hash of the missing block as an + /// array because the remaining code expects that, but the array will only + /// ever have at most one element. + pub(super) fn missing_parents( + &self, + conn: &PgConnection, + chain: &str, + first_block: i64, + hash: H256, + genesis: H256, + ) -> Result, Error> { + match self { + Storage::Shared => { + // We recursively build a temp table 'chain' containing the hash and + // parent_hash of blocks to check. The 'last' value is used to stop + // the recursion and is true if one of these conditions is true: + // * we are missing a parent block + // * we checked the required number of blocks + // * we checked the genesis block + const MISSING_PARENT_SQL: &str = " + with recursive chain(hash, parent_hash, last) as ( + -- base case: look at the head candidate block + select b.hash, b.parent_hash, false + from ethereum_blocks b + where b.network_name = $1 + and b.hash = $2 + and b.hash != $3 + union all + -- recursion step: add a block whose hash is the latest parent_hash + -- on chain + select chain.parent_hash, + b.parent_hash, + coalesce(b.parent_hash is null + or b.number <= $4 + or b.hash = $3, true) + from chain left outer join ethereum_blocks b + on chain.parent_hash = b.hash + and b.network_name = $1 + where not chain.last) + select hash + from chain + where chain.parent_hash is null; + "; -use crate::functions::{attempt_chain_head_update, lookup_ancestor_block}; -use crate::{chain_head_listener::ChainHeadUpdateListener, connection_pool::ConnectionPool}; + let hash = format!("{:x}", hash); + let genesis = format!("{:x}", genesis); + let missing = sql_query(MISSING_PARENT_SQL) + .bind::(chain) + .bind::(&hash) + .bind::(&genesis) + .bind::(first_block) + .load::(conn)?; + + missing + .into_iter() + .map(|parent| parent.hash.parse()) + .collect::>() + .map_err(Error::from) + } + Storage::Private(Schema { blocks, .. }) => { + // This is the same as `MISSING_PARENT_SQL` above except that + // the blocks table has a different name and that it does + // not have a `network_name` column + let query = format!( + " + with recursive chain(hash, parent_hash, last) as ( + -- base case: look at the head candidate block + select b.hash, b.parent_hash, false + from {qname} b + where b.hash = $1 + and b.hash != $2 + union all + -- recursion step: add a block whose hash is the latest parent_hash + -- on chain + select chain.parent_hash, + b.parent_hash, + coalesce(b.parent_hash is null + or b.number <= $3 + or b.hash = $2, true) + from chain left outer join {qname} b + on chain.parent_hash = b.hash + where not chain.last) + select hash + from chain + where chain.parent_hash is null; + ", + qname = blocks.qname + ); + + let missing = sql_query(query) + .bind::(hash.as_bytes()) + .bind::(genesis.as_bytes()) + .bind::(first_block) + .load::(conn)?; + + missing + .into_iter() + .map(|parent| h256_from_bytes(&parent.hash)) + .collect::>() + .map_err(Error::from) + } + } + } + + /// Return the best candidate for the new chain head if there is a block + /// with a higher block number than the current chain head. The returned + /// value if the hash and number of the candidate and the genesis block + /// hash for the chain + pub(super) fn chain_head_candidate( + &self, + conn: &PgConnection, + chain: &str, + ) -> Result, Error> { + use public::ethereum_networks as n; + + let head = n::table + .filter(n::name.eq(chain)) + .select(n::head_block_number) + .first::>(conn)? + .unwrap_or(-1); + + match self { + Storage::Shared => { + use public::ethereum_blocks as b; + b::table + .filter(b::network_name.eq(chain)) + .filter(b::number.gt(head)) + .order_by((b::number.desc(), b::hash)) + .select((b::hash, b::number)) + .first::<(String, i64)>(conn) + .optional()? + .map(|(hash, number)| { + EthereumBlockPointer::try_from((hash.as_str(), number)) + }) + .transpose() + } + Storage::Private(Schema { blocks, .. }) => blocks + .table() + .filter(blocks.number().gt(head)) + .order_by((blocks.number().desc(), blocks.hash())) + .select((blocks.hash(), blocks.number())) + .first::<(Vec, i64)>(conn) + .optional()? + .map(|(hash, number)| EthereumBlockPointer::try_from((hash.as_slice(), number))) + .transpose(), + } + } + + pub(super) fn ancestor_block( + &self, + conn: &PgConnection, + block_ptr: EthereumBlockPointer, + offset: u64, + ) -> Result, Error> { + let data = match self { + Storage::Shared => { + const ANCESTOR_SQL: &str = " + with recursive ancestors(block_hash, block_offset) as ( + values ($1, 0) + union all + select b.parent_hash, a.block_offset+1 + from ancestors a, ethereum_blocks b + where a.block_hash = b.hash + and a.block_offset < $2 + ) + select a.block_hash as hash + from ancestors a + where a.block_offset = $2;"; + + let hash = sql_query(ANCESTOR_SQL) + .bind::(block_ptr.hash_hex()) + .bind::(offset as i64) + .get_result::(conn) + .optional()?; + + use public::ethereum_blocks as b; + + match hash { + None => None, + Some(hash) => Some( + b::table + .filter(b::hash.eq(hash.hash)) + .select(b::data) + .first::(conn)?, + ), + } + } + Storage::Private(Schema { blocks, .. }) => { + // Same as ANCESTOR_SQL except for the table name + let query = format!( + " + with recursive ancestors(block_hash, block_offset) as ( + values ($1, 0) + union all + select b.parent_hash, a.block_offset+1 + from ancestors a, {} b + where a.block_hash = b.hash + and a.block_offset < $2 + ) + select a.block_hash as hash + from ancestors a + where a.block_offset = $2;", + blocks.qname + ); + + let hash = sql_query(query) + .bind::(block_ptr.hash.as_bytes()) + .bind::(offset as i64) + .get_result::(conn) + .optional()?; + match hash { + None => None, + Some(hash) => Some( + blocks + .table() + .filter(blocks.hash().eq(hash.hash)) + .select(blocks.data()) + .first::(conn)?, + ), + } + } + }; + + let block = data + .map(|data| serde_json::from_value::(data)) + .transpose() + .expect("Failed to deserialize block from database"); + + Ok(block) + } + + pub(super) fn delete_blocks_before( + &self, + conn: &PgConnection, + chain: &str, + block: i64, + ) -> Result { + match self { + Storage::Shared => { + use public::ethereum_blocks as b; + + diesel::delete(b::table) + .filter(b::network_name.eq(chain)) + .filter(b::number.lt(block)) + .filter(b::number.gt(0)) + .execute(conn) + .map_err(Error::from) + } + Storage::Private(Schema { blocks, .. }) => { + let query = format!( + "delete from {} where number < $1 and number > 0", + blocks.qname + ); + sql_query(query) + .bind::(block) + .execute(conn) + .map_err(Error::from) + } + } + } + + pub(super) fn get_call_and_access( + &self, + conn: &PgConnection, + id: &[u8], + ) -> Result, bool)>, Error> { + match self { + Storage::Shared => { + use public::eth_call_cache as cache; + use public::eth_call_meta as meta; + + cache::table + .find(id.as_ref()) + .inner_join(meta::table) + .select(( + cache::return_value, + sql("CURRENT_DATE > eth_call_meta.accessed_at"), + )) + .get_result(conn) + .optional() + .map_err(Error::from) + } + Storage::Private(Schema { + call_cache, + call_meta, + .. + }) => call_cache + .table() + .inner_join( + call_meta.table().on(call_meta + .contract_address() + .eq(call_cache.contract_address())), + ) + .filter(call_cache.id().eq(id)) + .select(( + call_cache.return_value(), + sql(&format!( + "CURRENT_DATE > {}.{}", + CallMetaTable::TABLE_NAME, + CallMetaTable::ACCESSED_AT + )), + )) + .first(conn) + .optional() + .map_err(Error::from), + } + } + + pub(super) fn update_accessed_at( + &self, + conn: &PgConnection, + contract_address: &[u8], + ) -> Result<(), Error> { + let result = match self { + Storage::Shared => { + use public::eth_call_meta as meta; + + update(meta::table.find(contract_address.as_ref())) + .set(meta::accessed_at.eq(sql("CURRENT_DATE"))) + .execute(conn) + } + Storage::Private(Schema { call_meta, .. }) => { + let query = format!( + "update {} set accessed_at = CURRENT_DATE where contract_address = $1", + call_meta.qname + ); + sql_query(query) + .bind::(contract_address) + .execute(conn) + } + }; + result.map(|_| ()).map_err(Error::from) + } + + pub(super) fn set_call( + &self, + conn: &PgConnection, + id: &[u8], + contract_address: &[u8], + block_number: i32, + return_value: &[u8], + ) -> Result<(), Error> { + let result = match self { + Storage::Shared => { + use public::eth_call_cache as cache; + use public::eth_call_meta as meta; + + insert_into(cache::table) + .values(( + cache::id.eq(id), + cache::contract_address.eq(contract_address), + cache::block_number.eq(block_number), + cache::return_value.eq(return_value), + )) + .on_conflict_do_nothing() + .execute(conn)?; + + let accessed_at = meta::accessed_at.eq(sql("CURRENT_DATE")); + insert_into(meta::table) + .values(( + meta::contract_address.eq(contract_address.as_ref()), + accessed_at.clone(), + )) + .on_conflict(meta::contract_address) + .do_update() + .set(accessed_at) + .execute(conn) + } + Storage::Private(Schema { + call_cache, + call_meta, + .. + }) => { + let query = format!( + "insert into {}(id, contract_address, block_number, return_value) \ + values ($1, $2, $3, $4) on conflict do nothing", + call_cache.qname + ); + sql_query(query) + .bind::(id) + .bind::(contract_address) + .bind::(block_number) + .bind::(return_value) + .execute(conn)?; + + let query = format!( + "insert into {}(contract_address, accessed_at) \ + values ($1, CURRENT_DATE) \ + on conflict(contract_address) do update set accessed_at = CURRENT_DATE", + call_meta.qname + ); + sql_query(query) + .bind::(contract_address) + .execute(conn) + } + }; + result.map(|_| ()).map_err(Error::from) + } + + #[cfg(debug_assertions)] + // used by `super::set_chain` for test support + pub(super) fn set_chain( + &self, + conn: &PgConnection, + chain_name: &str, + genesis_hash: &str, + chain: super::test_support::Chain, + ) { + use public::ethereum_networks as n; + + match self { + Storage::Shared => { + use public::eth_call_cache as c; + use public::eth_call_meta as m; + use public::ethereum_blocks as b; + + diesel::delete(b::table.filter(b::network_name.eq(chain_name))) + .execute(conn) + .expect("Failed to delete ethereum_blocks"); + // We don't have a good way to clean out the call cache + // per chain; just nuke everything + diesel::delete(c::table).execute(conn).unwrap(); + diesel::delete(m::table).execute(conn).unwrap(); + } + Storage::Private(Schema { + blocks, + call_meta, + call_cache, + .. + }) => { + for qname in &[&blocks.qname, &call_meta.qname, &call_cache.qname] { + let query = format!("delete from {}", qname); + sql_query(query) + .execute(conn) + .expect(&format!("Failed to delete {}", qname)); + } + } + } + + for block in &chain { + self.upsert_block(conn, chain_name, block.as_ethereum_block()) + .unwrap(); + } + + diesel::update(n::table.filter(n::name.eq(chain_name))) + .set(( + n::genesis_block_hash.eq(genesis_hash), + n::head_block_hash.eq::>(None), + n::head_block_number.eq::>(None), + )) + .execute(conn) + .unwrap(); + } + } +} pub struct ChainStore { conn: ConnectionPool, - network: String, + pub chain: String, + storage: data::Storage, genesis_block_ptr: EthereumBlockPointer, chain_head_update_listener: Arc, + chain_head_update_sender: ChainHeadUpdateSender, } impl ChainStore { - pub fn new( - network: String, + pub(crate) fn new( + chain: String, + storage: data::Storage, net_identifier: EthereumNetworkIdentifier, chain_head_update_listener: Arc, + chain_head_update_sender: ChainHeadUpdateSender, pool: ConnectionPool, ) -> Self { let store = ChainStore { conn: pool, - network, + chain, + storage, genesis_block_ptr: (net_identifier.genesis_block_hash, 0 as u64).into(), chain_head_update_listener, + chain_head_update_sender, }; // Add network to store and check network identifiers @@ -55,36 +1096,40 @@ impl ChainStore { &self, new_net_identifiers: EthereumNetworkIdentifier, ) -> Result<(), Error> { - use crate::db_schema::ethereum_networks::dsl::*; + use public::ethereum_networks::dsl::*; let new_genesis_block_hash = new_net_identifiers.genesis_block_hash; let new_net_version = new_net_identifiers.net_version; let network_identifiers_opt = ethereum_networks .select((net_version, genesis_block_hash)) - .filter(name.eq(&self.network)) - .first::<(Option, Option)>(&*self.get_conn()?) + .filter(name.eq(&self.chain)) + .first::<(String, String)>(&*self.get_conn()?) .optional()?; + let conn = self.get_conn()?; match network_identifiers_opt { // Network is missing in database None => { - insert_into(ethereum_networks) - .values(( - name.eq(&self.network), - head_block_hash.eq::>(None), - head_block_number.eq::>(None), - net_version.eq::>(Some(new_net_version.to_owned())), - genesis_block_hash - .eq::>(Some(format!("{:x}", new_genesis_block_hash))), - )) - .on_conflict(name) - .do_nothing() - .execute(&*self.get_conn()?)?; + conn.transaction(|| { + insert_into(ethereum_networks) + .values(( + name.eq(&self.chain), + namespace.eq(&self.storage), + head_block_hash.eq::>(None), + head_block_number.eq::>(None), + net_version.eq(new_net_version), + genesis_block_hash.eq(format!("{:x}", new_genesis_block_hash)), + )) + .on_conflict(name) + .do_nothing() + .execute(&conn)?; + self.storage.create(&conn) + })?; } // Network is in database and has identifiers - Some((Some(last_net_version), Some(last_genesis_block_hash))) => { + Some((last_net_version, last_genesis_block_hash)) => { if last_net_version != new_net_version { panic!( "Ethereum node provided net_version {}, \ @@ -103,22 +1148,50 @@ impl ChainStore { ); } } - - // Network is in database but is missing identifiers - Some(_) => { - update(ethereum_networks) - .set(( - net_version.eq::>(Some(new_net_version.to_owned())), - genesis_block_hash - .eq::>(Some(format!("{:x}", new_genesis_block_hash))), - )) - .filter(name.eq(&self.network)) - .execute(&*self.get_conn()?)?; - } } Ok(()) } + + pub fn chain_head_pointers(&self) -> Result, StoreError> { + use public::ethereum_networks as n; + + let pointers: Vec<(String, EthereumBlockPointer)> = n::table + .select((n::name, n::head_block_hash, n::head_block_number)) + .load::<(String, Option, Option)>(&self.get_conn()?)? + .into_iter() + .filter_map(|(name, hash, number)| match (hash, number) { + (Some(hash), Some(number)) => Some((name, hash, number)), + _ => None, + }) + .map(|(name, hash, number)| { + EthereumBlockPointer::try_from((hash.as_str(), number)).map(|ptr| (name, ptr)) + }) + .collect::, _>>()?; + Ok(HashMap::from_iter(pointers)) + } + + pub fn chain_head_block(&self, chain: &str) -> Result, StoreError> { + use public::ethereum_networks as n; + + let number: Option = n::table + .filter(n::name.eq(chain)) + .select(n::head_block_number) + .first::>(&self.get_conn()?) + .optional()? + .flatten(); + + number.map(|number| number.try_into()).transpose().map_err( + |e: std::num::TryFromIntError| { + constraint_violation!( + "head block number for {} is {:?} which does not fit into a u32: {}", + chain, + number, + e.to_string() + ) + }, + ) + } } impl ChainStoreTrait for ChainStore { @@ -134,102 +1207,77 @@ impl ChainStoreTrait for ChainStore { B: Stream + Send + 'static, E: From + Send + 'static, { - use crate::db_schema::ethereum_blocks::dsl::*; - let conn = self.conn.clone(); - let net_name = self.network.clone(); + let network = self.chain.clone(); + let storage = self.storage.clone(); Box::new(blocks.for_each(move |block| { - let json_blob = serde_json::to_value(&block).expect("Failed to serialize block"); - let values = ( - hash.eq(format!("{:x}", block.block.hash.unwrap())), - number.eq(block.block.number.unwrap().as_u64() as i64), - parent_hash.eq(format!("{:x}", block.block.parent_hash)), - network_name.eq(&net_name), - data.eq(json_blob), - ); - - // Insert blocks. - // - // If the table already contains a block with the same hash, then overwrite that block - // if it may be adding transaction receipts. - insert_into(ethereum_blocks) - .values(values.clone()) - .on_conflict(hash) - .do_update() - .set(values) - .execute(&*conn.get().map_err(Error::from)?) - .map_err(Error::from) + let conn = conn.get().map_err(Error::from)?; + storage + .upsert_block(&conn, &network, block) .map_err(E::from) - .map(|_| ()) })) } fn upsert_light_blocks(&self, blocks: Vec) -> Result<(), Error> { - use crate::db_schema::ethereum_blocks::dsl::*; - - let conn = self.conn.clone(); - let net_name = self.network.clone(); + let conn = self.conn.get()?; for block in blocks { - let block_hash = format!("{:x}", block.hash.unwrap()); - let p_hash = format!("{:x}", block.parent_hash); - let block_number = block.number.unwrap().as_u64(); - let json_blob = serde_json::to_value(&EthereumBlock { - block, - transaction_receipts: Vec::new(), - }) - .expect("Failed to serialize block"); - let values = ( - hash.eq(block_hash), - number.eq(block_number as i64), - parent_hash.eq(p_hash), - network_name.eq(&net_name), - data.eq(json_blob), - ); - - // Insert blocks. On conflict do nothing, we don't want to erase transaction receipts. - insert_into(ethereum_blocks) - .values(values.clone()) - .on_conflict(hash) - .do_nothing() - .execute(&*conn.get()?)?; + self.storage.upsert_light_block(&conn, &self.chain, block)?; } Ok(()) } fn attempt_chain_head_update(&self, ancestor_count: u64) -> Result, Error> { - // Call attempt_head_update SQL function - select(attempt_chain_head_update( - &self.network, - ancestor_count as i64, - )) - .load(&*self.get_conn()?) - .map_err(Error::from) - // We got a single return value, but it's returned generically as a set of rows - .map(|mut rows: Vec<_>| { - assert_eq!(rows.len(), 1); - rows.pop().unwrap() - }) - // Parse block hashes into H256 type - .map(|hashes: Vec| { - hashes - .into_iter() - .map(|h| h.parse()) - .collect::, _>>() - }) - .and_then(|r| r.map_err(Error::from)) + use public::ethereum_networks as n; + + let (missing, ptr) = { + let conn = self.get_conn()?; + conn.transaction(|| -> Result<(Vec, Option<(String, i64)>), Error> { + let candidate = self.storage.chain_head_candidate(&conn, &self.chain)?; + let (ptr, first_block) = match candidate { + None => return Ok((vec![], None)), + Some(ptr) => (ptr, 0.max(ptr.number.saturating_sub(ancestor_count))), + }; + + let missing = self.storage.missing_parents( + &conn, + &self.chain, + first_block as i64, + ptr.hash, + self.genesis_block_ptr.hash, + )?; + if !missing.is_empty() { + return Ok((missing, None)); + } + + let hash = ptr.hash_hex(); + let number = ptr.number as i64; + update(n::table.filter(n::name.eq(&self.chain))) + .set(( + n::head_block_hash.eq(&hash), + n::head_block_number.eq(number), + )) + .execute(&conn)?; + Ok((missing, Some((hash, number)))) + }) + }?; + if let Some((hash, number)) = ptr { + self.chain_head_update_sender.send(&hash, number)?; + } + + Ok(missing) } fn chain_head_updates(&self) -> ChainHeadUpdateStream { self.chain_head_update_listener - .subscribe(self.network.to_owned()) + .subscribe(self.chain.to_owned()) } fn chain_head_ptr(&self) -> Result, Error> { - use crate::db_schema::ethereum_networks::dsl::*; + use public::ethereum_networks::dsl::*; ethereum_networks .select((head_block_hash, head_block_number)) - .filter(name.eq(&self.network)) + .filter(name.eq(&self.chain)) .load::<(Option, Option)>(&*self.get_conn()?) .map(|rows| { rows.first() @@ -244,20 +1292,8 @@ impl ChainStoreTrait for ChainStore { } fn blocks(&self, hashes: Vec) -> Result, Error> { - use crate::db_schema::ethereum_blocks::dsl::*; - use diesel::dsl::{any, sql}; - use diesel::sql_types::Jsonb; - - ethereum_blocks - .select(sql::("data -> 'block'")) - .filter(network_name.eq(&self.network)) - .filter(hash.eq(any(Vec::from_iter( - hashes.into_iter().map(|h| format!("{:x}", h)), - )))) - .load::(&*self.get_conn()?)? - .into_iter() - .map(|block| serde_json::from_value(block).map_err(Into::into)) - .collect() + let conn = self.get_conn()?; + self.storage.blocks(&conn, &self.chain, hashes) } fn ancestor_block( @@ -267,23 +1303,17 @@ impl ChainStoreTrait for ChainStore { ) -> Result, Error> { ensure!( block_ptr.number >= offset, - "block offset points to before genesis block" + "block offset {} for block `{}` points to before genesis block", + offset, + block_ptr.hash_hex() ); - select(lookup_ancestor_block(block_ptr.hash_hex(), offset as i64)) - .first::>(&*self.get_conn()?) - .map(|val_opt| { - val_opt.map(|val| { - serde_json::from_value::(val) - .expect("Failed to deserialize block from database") - }) - }) - .map_err(Error::from) + let conn = self.get_conn()?; + self.storage.ancestor_block(&conn, block_ptr, offset) } fn cleanup_cached_blocks(&self, ancestor_count: u64) -> Result<(BlockNumber, usize), Error> { - use crate::db_schema::ethereum_blocks::dsl; - use diesel::sql_types::{Integer, Text}; + use diesel::sql_types::Integer; #[derive(QueryableByName)] struct MinBlock { @@ -328,7 +1358,7 @@ impl ChainStoreTrait for ChainStore { .expect("ancestor_count fits into a signed 32 bit integer"); diesel::sql_query(query) .bind::(ancestor_count) - .bind::(&self.network) + .bind::(&self.chain) .load::(&conn)? .first() .map(|MinBlock { block }| { @@ -336,11 +1366,8 @@ impl ChainStoreTrait for ChainStore { // returns -1, and we should not do anything. We also guard // against removing the genesis block if *block > 0 { - diesel::delete(dsl::ethereum_blocks) - .filter(dsl::network_name.eq(&self.network)) - .filter(dsl::number.lt(*block as i64)) - .filter(dsl::number.gt(0)) - .execute(&conn) + self.storage + .delete_blocks_before(&conn, &self.chain, *block as i64) .map(|rows| (*block, rows)) } else { Ok((0, 0)) @@ -351,46 +1378,165 @@ impl ChainStoreTrait for ChainStore { } fn block_hashes_by_block_number(&self, number: u64) -> Result, Error> { - use crate::db_schema::ethereum_blocks::dsl; - let conn = self.get_conn()?; - dsl::ethereum_blocks - .select(dsl::hash) - .filter(dsl::network_name.eq(&self.network)) - .filter(dsl::number.eq(number as i64)) - .get_results::(&conn)? - .into_iter() - .map(|h| h.parse()) - .collect::, _>>() - .map_err(Error::from) + self.storage + .block_hashes_by_block_number(&conn, &self.chain, number) } fn confirm_block_hash(&self, number: u64, hash: &H256) -> Result { - use crate::db_schema::ethereum_blocks::dsl; - let conn = self.get_conn()?; - diesel::delete(dsl::ethereum_blocks) - .filter(dsl::network_name.eq(&self.network)) - .filter(dsl::number.eq(number as i64)) - .filter(dsl::hash.ne(&format!("{:x}", hash))) - .execute(&conn) - .map_err(Error::from) + self.storage + .confirm_block_hash(&conn, &self.chain, number, hash) } fn block_number(&self, hash: H256) -> Result, StoreError> { - use crate::db_schema::ethereum_blocks::dsl; - let conn = self.get_conn()?; - dsl::ethereum_blocks - .select((dsl::network_name, dsl::number)) - .filter(dsl::hash.eq(format!("{:x}", hash))) - .first::<(String, i64)>(&conn) - .optional()? - .map(|(name, number)| { - BlockNumber::try_from(number) - .map(|number| (name, number)) - .map_err(|e| StoreError::QueryExecutionError(e.to_string())) - }) - .transpose() + Ok(self + .storage + .block_number(&conn, hash)? + .map(|number| (self.chain.clone(), number))) + } +} + +impl EthereumCallCache for ChainStore { + fn get_call( + &self, + contract_address: ethabi::Address, + encoded_call: &[u8], + block: EthereumBlockPointer, + ) -> Result>, Error> { + let id = contract_call_id(&contract_address, encoded_call, &block); + let conn = &*self.get_conn()?; + if let Some(call_output) = conn.transaction::<_, Error, _>(|| { + if let Some((return_value, update_accessed_at)) = + self.storage.get_call_and_access(conn, id.as_ref())? + { + if update_accessed_at { + self.storage + .update_accessed_at(conn, contract_address.as_ref())?; + } + Ok(Some(return_value)) + } else { + Ok(None) + } + })? { + Ok(Some(call_output)) + } else { + Ok(None) + } + } + + fn set_call( + &self, + contract_address: ethabi::Address, + encoded_call: &[u8], + block: EthereumBlockPointer, + return_value: &[u8], + ) -> Result<(), Error> { + let id = contract_call_id(&contract_address, encoded_call, &block); + let conn = &*self.get_conn()?; + conn.transaction(|| { + self.storage.set_call( + conn, + id.as_ref(), + contract_address.as_ref(), + block.number as i32, + return_value, + ) + }) + } +} + +/// The id is the hashed encoded_call + contract_address + block hash to uniquely identify the call. +/// 256 bits of output, and therefore 128 bits of security against collisions, are needed since this +/// could be targeted by a birthday attack. +fn contract_call_id( + contract_address: ðabi::Address, + encoded_call: &[u8], + block: &EthereumBlockPointer, +) -> [u8; 32] { + let mut hash = blake3::Hasher::new(); + hash.update(encoded_call); + hash.update(contract_address.as_ref()); + hash.update(block.hash.as_ref()); + *hash.finalize().as_bytes() +} + +/// Support for tests +#[cfg(debug_assertions)] +pub mod test_support { + use std::str::FromStr; + + use graph::prelude::{web3::types::H256, EthereumBlock, EthereumBlockPointer}; + + // Hash indicating 'no parent' + pub const NO_PARENT: &str = "0000000000000000000000000000000000000000000000000000000000000000"; + /// The parts of an Ethereum block that are interesting for these tests: + /// the block number, hash, and the hash of the parent block + #[derive(Clone, Debug, PartialEq)] + pub struct FakeBlock { + pub number: u64, + pub hash: String, + pub parent_hash: String, + } + + impl FakeBlock { + pub fn make_child(&self, hash: &str) -> Self { + FakeBlock { + number: self.number + 1, + hash: hash.to_owned(), + parent_hash: self.hash.clone(), + } + } + + pub fn make_no_parent(number: u64, hash: &str) -> Self { + FakeBlock { + number, + hash: hash.to_owned(), + parent_hash: NO_PARENT.to_string(), + } + } + + pub fn block_hash(&self) -> H256 { + H256::from_str(self.hash.as_str()).expect("invalid block hash") + } + + pub fn block_ptr(&self) -> EthereumBlockPointer { + EthereumBlockPointer { + number: self.number, + hash: self.block_hash(), + } + } + + pub fn as_ethereum_block(&self) -> EthereumBlock { + let parent_hash = + H256::from_str(self.parent_hash.as_str()).expect("invalid parent hash"); + + let mut block = EthereumBlock::default(); + block.block.number = Some(self.number.into()); + block.block.parent_hash = parent_hash; + block.block.hash = Some(self.block_hash()); + + block + } + } + + pub type Chain = Vec<&'static FakeBlock>; + + /// Store the given chain as the blocks for the `network` set the + /// network's genesis block to `genesis_hash`, and head block to + /// `null` + pub trait SettableChainStore { + fn set_chain(&self, genesis_hash: &str, chain: Chain); + } +} + +#[cfg(debug_assertions)] +impl test_support::SettableChainStore for ChainStore { + fn set_chain(&self, genesis_hash: &str, chain: test_support::Chain) { + let conn = self.conn.get().expect("can get a database connection"); + + self.storage + .set_chain(&conn, &self.chain, genesis_hash, chain); } } diff --git a/store/postgres/src/connection_pool.rs b/store/postgres/src/connection_pool.rs index cc8bd2a5be7..bc4b8c4e212 100644 --- a/store/postgres/src/connection_pool.rs +++ b/store/postgres/src/connection_pool.rs @@ -1,7 +1,11 @@ use diesel::pg::PgConnection; -use diesel::r2d2::{self, event as e, ConnectionManager, HandleEvent, Pool}; +use diesel::r2d2::{self, event as e, ConnectionManager, HandleEvent, Pool, PooledConnection}; -use graph::prelude::*; +use graph::prelude::{ + error, info, o, tokio::sync::Semaphore, CancelGuard, CancelHandle, CancelToken as _, + CancelableError, Counter, Gauge, Logger, MetricsRegistry, MovingStats, PoolWaitStats, + StoreError, +}; use std::fmt; use std::sync::Arc; @@ -11,12 +15,13 @@ use std::{collections::HashMap, sync::RwLock}; #[derive(Clone)] pub struct ConnectionPool { pool: Pool>, + limiter: Arc, pub(crate) wait_stats: PoolWaitStats, } struct ErrorHandler(Logger, Counter); -impl Debug for ErrorHandler { +impl std::fmt::Debug for ErrorHandler { fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result { fmt::Result::Ok(()) } @@ -76,7 +81,7 @@ impl EventHandler { } } -impl Debug for EventHandler { +impl std::fmt::Debug for EventHandler { fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result { fmt::Result::Ok(()) } @@ -164,7 +169,114 @@ impl ConnectionPool { .max_size(pool_size) .build(conn_manager) .unwrap(); + let limiter = Arc::new(Semaphore::new(pool_size as usize)); info!(logger_store, "Pool successfully connected to Postgres"); - ConnectionPool { pool, wait_stats } + ConnectionPool { + pool, + limiter, + wait_stats, + } + } + + /// Execute a closure with a connection to the database. + /// + /// # API + /// The API of using a closure to bound the usage of the connection serves several + /// purposes: + /// + /// * Moves blocking database access out of the `Future::poll`. Within + /// `Future::poll` (which includes all `async` methods) it is illegal to + /// perform a blocking operation. This includes all accesses to the + /// database, acquiring of locks, etc. Calling a blocking operation can + /// cause problems with `Future` combinators (including but not limited + /// to select, timeout, and FuturesUnordered) and problems with + /// executors/runtimes. This method moves the database work onto another + /// thread in a way which does not block `Future::poll`. + /// + /// * Limit the total number of connections. Because the supplied closure + /// takes a reference, we know the scope of the usage of all entity + /// connections and can limit their use in a non-blocking way. + /// + /// # Cancellation + /// The normal pattern for futures in Rust is drop to cancel. Once we + /// spawn the database work in a thread though, this expectation no longer + /// holds because the spawned task is the independent of this future. So, + /// this method provides a cancel token which indicates that the `Future` + /// has been dropped. This isn't *quite* as good as drop on cancel, + /// because a drop on cancel can do things like cancel http requests that + /// are in flight, but checking for cancel periodically is a significant + /// improvement. + /// + /// The implementation of the supplied closure should check for cancel + /// between every operation that is potentially blocking. This includes + /// any method which may interact with the database. The check can be + /// conveniently written as `token.check_cancel()?;`. It is low overhead + /// to check for cancel, so when in doubt it is better to have too many + /// checks than too few. + /// + /// # Panics: + /// * This task will panic if the supplied closure panics + /// * This task will panic if the supplied closure returns Err(Cancelled) + /// when the supplied cancel token is not cancelled. + pub(crate) async fn with_conn( + &self, + f: impl 'static + + Send + + FnOnce( + &PooledConnection>, + &CancelHandle, + ) -> Result>, + ) -> Result { + let _permit = self.limiter.acquire().await; + let pool = self.clone(); + + let cancel_guard = CancelGuard::new(); + let cancel_handle = cancel_guard.handle(); + + let result = graph::spawn_blocking_allow_panic(move || { + // It is possible time has passed between scheduling on the + // threadpool and being executed. Time to check for cancel. + cancel_handle.check_cancel()?; + + // A failure to establish a connection is propagated as though the + // closure failed. + let conn = pool + .get() + .map_err(|e| CancelableError::Error(StoreError::Unknown(e.into())))?; + + // It is possible time has passed while establishing a connection. + // Time to check for cancel. + cancel_handle.check_cancel()?; + + f(&conn, &cancel_handle) + }) + .await + .unwrap(); // Propagate panics, though there shouldn't be any. + + drop(cancel_guard); + + // Finding cancel isn't technically unreachable, since there is nothing + // stopping the supplied closure from returning Canceled even if the + // supplied handle wasn't canceled. That would be very unexpected, the + // doc comment for this function says we will panic in this scenario. + match result { + Ok(t) => Ok(t), + Err(CancelableError::Error(e)) => Err(e), + Err(CancelableError::Cancel) => panic!("The closure supplied to with_entity_conn must not return Err(Canceled) unless the supplied token was canceled."), + } + } + + pub fn get_with_timeout_warning( + &self, + logger: &Logger, + ) -> Result>, graph::prelude::Error> { + loop { + match self.get_timeout(Duration::from_secs(60)) { + Ok(conn) => return Ok(conn), + Err(e) => error!(logger, "Error checking out connection, retrying"; + "error" => e.to_string(), + ), + } + } } } diff --git a/store/postgres/src/db_schema.rs b/store/postgres/src/db_schema.rs deleted file mode 100644 index a6ebbc82bf4..00000000000 --- a/store/postgres/src/db_schema.rs +++ /dev/null @@ -1,55 +0,0 @@ -table! { - ethereum_networks (name) { - name -> Varchar, - head_block_hash -> Nullable, - head_block_number -> Nullable, - net_version -> Nullable, - genesis_block_hash -> Nullable, - } -} - -table! { - ethereum_blocks (hash) { - hash -> Varchar, - number -> BigInt, - parent_hash -> Nullable, - network_name -> Varchar, // REFERENCES ethereum_networks (name), - data -> Jsonb, - } -} - -table! { - large_notifications(id) { - id -> Integer, - payload -> Text, - created_at -> Timestamp, - } -} - -table! { - ens_names(hash) { - hash -> Varchar, - name -> Varchar, - } -} - -table! { - /// `id` is the hash of contract address + encoded function call + block number. - eth_call_cache (id) { - id -> Bytea, - return_value -> Bytea, - contract_address -> Bytea, - block_number -> Integer, - } -} - -table! { - /// When was a cached call on a contract last used? This is useful to clean old data. - eth_call_meta (contract_address) { - contract_address -> Bytea, - accessed_at -> Date, - } -} - -joinable!(eth_call_cache -> eth_call_meta (contract_address)); -allow_tables_to_appear_in_same_query!(eth_call_cache, eth_call_meta); diff --git a/store/postgres/src/deployment.rs b/store/postgres/src/deployment.rs index c225584b107..60a4c8a8ba0 100644 --- a/store/postgres/src/deployment.rs +++ b/store/postgres/src/deployment.rs @@ -37,6 +37,19 @@ pub enum SubgraphHealth { Unhealthy, } +impl From for graph::data::subgraph::schema::SubgraphHealth { + fn from(health: SubgraphHealth) -> Self { + use graph::data::subgraph::schema::SubgraphHealth as H; + use SubgraphHealth as Db; + + match health { + Db::Failed => H::Failed, + Db::Healthy => H::Healthy, + Db::Unhealthy => H::Unhealthy, + } + } +} + table! { subgraphs.subgraph_deployment (vid) { vid -> BigInt, @@ -121,6 +134,8 @@ table! { } } +allow_tables_to_appear_in_same_query!(subgraph_deployment, subgraph_error); + /// Look up the graft point for the given subgraph in the database and /// return it. If `pending_only` is `true`, only return `Some(_)` if the /// deployment has not progressed past the graft point, i.e., data has not @@ -213,30 +228,6 @@ pub fn manifest_info( .map(|schema| (schema, description, repository)) } -pub fn network( - conn: &PgConnection, - id: &SubgraphDeploymentId, -) -> Result, StoreError> { - use ethereum_contract_data_source as ds; - use subgraph_manifest as sm; - - let manifest_id = SubgraphManifestEntity::id(&id); - let data_sources: Vec = sm::table - .select(sm::data_sources) - .filter(sm::id.eq(manifest_id.as_str())) - .first::>(conn)?; - // The NetworkIndexer creates a manifest with an empty - // array of data sources and we therefore accept 'None' - // here - match data_sources.first() { - Some(ds_id) => Ok(ds::table - .select(ds::network) - .filter(ds::id.eq(&ds_id)) - .first::>(conn)?), - None => Ok(None), - } -} - pub fn features( conn: &PgConnection, id: &SubgraphDeploymentId, @@ -531,6 +522,7 @@ pub(crate) fn has_non_fatal_errors( /// healthy or unhealthy depending on whether it also had non-fatal errors pub fn unfail(conn: &PgConnection, id: &SubgraphDeploymentId) -> Result<(), StoreError> { use subgraph_deployment as d; + use subgraph_error as e; use SubgraphHealth::*; let prev_health = if has_non_fatal_errors(conn, id, None)? { @@ -539,18 +531,29 @@ pub fn unfail(conn: &PgConnection, id: &SubgraphDeploymentId) -> Result<(), Stor Healthy }; - // The update does nothing unless the subgraph is in state 'failed' - update( - d::table - .filter(d::id.eq(id.as_str())) - .filter(d::health.eq(Failed)), - ) - .set(( - d::failed.eq(false), - d::health.eq(prev_health), - d::fatal_error.eq::>(None), - )) - .execute(conn)?; + let fatal_error_id = match d::table + .filter(d::id.eq(id.as_str())) + .select(d::fatal_error) + .get_result::>(conn)? + { + Some(fatal_error_id) => fatal_error_id, + + // If the subgraph is not failed then there is nothing to do. + None => return Ok(()), + }; + + // Unfail the deployment. + update(d::table.filter(d::id.eq(id.as_str()))) + .set(( + d::failed.eq(false), + d::health.eq(prev_health), + d::fatal_error.eq::>(None), + )) + .execute(conn)?; + + // Delete the fatal error. + delete(e::table.filter(e::id.eq(fatal_error_id))).execute(conn)?; + Ok(()) } @@ -559,12 +562,13 @@ pub(crate) fn insert_subgraph_errors( conn: &PgConnection, id: &SubgraphDeploymentId, deterministic_errors: Vec, + block: BlockNumber, ) -> Result<(), StoreError> { for error in deterministic_errors { insert_subgraph_error(conn, error)?; } - check_health(conn, id) + check_health(conn, id, block) } #[cfg(debug_assertions)] @@ -580,13 +584,16 @@ pub(crate) fn error_count( .get_result::(conn)? as usize) } -/// Checks if the subgraph is healthy or unhealthy as of the latest block, based on the presence of -/// deterministic errors. Has no effect on failed subgraphs. -fn check_health(conn: &PgConnection, id: &SubgraphDeploymentId) -> Result<(), StoreError> { +/// Checks if the subgraph is healthy or unhealthy as of the given block, or the subgraph latest +/// block if `None`, based on the presence of non-fatal errors. Has no effect on failed subgraphs. +fn check_health( + conn: &PgConnection, + id: &SubgraphDeploymentId, + block: BlockNumber, +) -> Result<(), StoreError> { use subgraph_deployment as d; - // Errors have unbounded upper bounds so if one exists, it exists as of the latest block. - let has_errors = has_non_fatal_errors(conn, id, None)?; + let has_errors = has_non_fatal_errors(conn, id, Some(block))?; let (new, old) = match has_errors { true => (SubgraphHealth::Unhealthy, SubgraphHealth::Healthy), @@ -620,7 +627,10 @@ pub(crate) fn revert_subgraph_errors( ) .execute(conn)?; - check_health(conn, id) + // The result will be the same at `reverted_block` or `reverted_block - 1` since the errors at + // `reverted_block` were just deleted, but semantically we care about `reverted_block - 1` which + // is the block being reverted to. + check_health(conn, id, reverted_block - 1) } /// Drop the schema `namespace`. This deletes all data for the subgraph, diff --git a/store/postgres/src/deployment_store.rs b/store/postgres/src/deployment_store.rs new file mode 100644 index 00000000000..ffea03c3ae1 --- /dev/null +++ b/store/postgres/src/deployment_store.rs @@ -0,0 +1,1132 @@ +use detail::DeploymentDetail; +use diesel::connection::SimpleConnection; +use diesel::pg::PgConnection; +use diesel::prelude::*; +use diesel::r2d2::{ConnectionManager, PooledConnection}; +use futures03::FutureExt as _; +use graph::components::store::{EntityType, StoredDynamicDataSource}; +use graph::data::subgraph::status; +use graph::prelude::{ + error, CancelHandle, CancelToken, CancelableError, PoolWaitStats, SubgraphDeploymentEntity, +}; +use lru_time_cache::LruCache; +use rand::{seq::SliceRandom, thread_rng}; +use std::collections::{BTreeMap, HashMap}; +use std::convert::TryInto; +use std::iter::FromIterator; +use std::ops::Deref; +use std::sync::{atomic::AtomicUsize, Arc, Mutex}; +use std::time::Instant; + +use graph::components::store::EntityCollection; +use graph::components::subgraph::ProofOfIndexingFinisher; +use graph::data::subgraph::schema::{SubgraphError, POI_OBJECT}; +use graph::prelude::{ + anyhow, debug, futures03, info, o, web3, ApiSchema, BlockNumber, CheapClone, DeploymentState, + DynTryFuture, Entity, EntityKey, EntityModification, EntityOrder, EntityQuery, EntityRange, + Error, EthereumBlockPointer, Logger, MetadataOperation, MetricsRegistry, QueryExecutionError, + Schema, StopwatchMetrics, StoreError, StoreEvent, SubgraphDeploymentId, Value, + BLOCK_NUMBER_MAX, +}; + +use graph_graphql::prelude::api_schema; +use web3::types::Address; + +use crate::primary::Site; +use crate::relational::{Layout, METADATA_LAYOUT}; +use crate::relational_queries::FromEntityData; +use crate::{connection_pool::ConnectionPool, detail, entities as e}; +use crate::{deployment, primary::Namespace}; + +embed_migrations!("./migrations"); + +/// Run all schema migrations. +/// +/// When multiple `graph-node` processes start up at the same time, we ensure +/// that they do not run migrations in parallel by using `blocking_conn` to +/// serialize them. The `conn` is used to run the actual migration. +fn initiate_schema(logger: &Logger, conn: &PgConnection, blocking_conn: &PgConnection) { + // Collect migration logging output + let mut output = vec![]; + + // Make sure the locking table exists so we have something + // to lock. We intentionally ignore errors here, because they are most + // likely caused by us losing a race to create the table against another + // graph-node. If this truly is an error, we will trip over it when + // we try to lock the table and report it to the user + if let Err(e) = blocking_conn.batch_execute( + "create table if not exists \ + __graph_node_global_lock(id int)", + ) { + debug!( + logger, + "Creating lock table failed, this is most likely harmless"; + "error" => format!("{:?}", e) + ); + } + + // blocking_conn holds the lock on the migrations table for the duration + // of the migration on conn. Since all nodes execute this code, only one + // of them can run this code at the same time. We need to use two + // connections for this because diesel will run each migration in its + // own txn, which makes it impossible to hold a lock across all of them + // on that connection + info!( + logger, + "Waiting for other graph-node instances to finish migrating" + ); + let result = blocking_conn.transaction(|| { + diesel::sql_query("lock table __graph_node_global_lock in exclusive mode") + .execute(blocking_conn)?; + info!(logger, "Running migrations"); + embedded_migrations::run_with_output(conn, &mut output) + }); + info!(logger, "Migrations finished"); + + // If there was any migration output, log it now + let has_output = !output.is_empty(); + if has_output { + let msg = String::from_utf8(output).unwrap_or_else(|_| String::from("")); + if result.is_err() { + error!(logger, "Postgres migration output"; "output" => msg); + } else { + debug!(logger, "Postgres migration output"; "output" => msg); + } + } + + if let Err(e) = result { + panic!( + "Error setting up Postgres database: \ + You may need to drop and recreate your database to work with the \ + latest version of graph-node. Error information: {:?}", + e + ) + }; + + if has_output { + // We take getting output as a signal that a migration was actually + // run, which is not easy to tell from the Diesel API, and reset the + // query statistics since a schema change makes them not all that + // useful. An error here is not serious and can be ignored. + conn.batch_execute("select pg_stat_statements_reset()").ok(); + } +} + +/// When connected to read replicas, this allows choosing which DB server to use for an operation. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ReplicaId { + /// The main server has write and read access. + Main, + + /// A read replica identified by its index. + ReadOnly(usize), +} + +/// Commonly needed information about a subgraph that we cache in +/// `Store.subgraph_cache`. Only immutable subgraph data can be cached this +/// way as the cache lives for the lifetime of the `Store` object +#[derive(Clone)] +pub(crate) struct SubgraphInfo { + /// The schema as supplied by the user + pub(crate) input: Arc, + /// The schema we derive from `input` with `graphql::schema::api::api_schema` + pub(crate) api: Arc, + /// The block number at which this subgraph was grafted onto + /// another one. We do not allow reverting past this block + pub(crate) graft_block: Option, + pub(crate) description: Option, + pub(crate) repository: Option, +} + +pub struct StoreInner { + logger: Logger, + + conn: ConnectionPool, + read_only_pools: Vec, + replica_order: Vec, + conn_round_robin_counter: AtomicUsize, + + /// A cache of commonly needed data about a subgraph. + subgraph_cache: Mutex>, + + /// A cache for the layout metadata for subgraphs. The Store just + /// hosts this because it lives long enough, but it is managed from + /// the entities module + pub(crate) layout_cache: e::LayoutCache, + + registry: Arc, +} + +/// Storage of the data for individual deployments. Each `DeploymentStore` +/// corresponds to one of the database shards that `SubgraphStore` manages. +#[derive(Clone)] +pub struct DeploymentStore(Arc); + +impl CheapClone for DeploymentStore {} + +impl Deref for DeploymentStore { + type Target = StoreInner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DeploymentStore { + pub fn new( + logger: &Logger, + pool: ConnectionPool, + read_only_pools: Vec, + mut pool_weights: Vec, + registry: Arc, + ) -> Self { + // Create a store-specific logger + let logger = logger.new(o!("component" => "Store")); + + // Create the entities table (if necessary) + initiate_schema(&logger, &pool.get().unwrap(), &pool.get().unwrap()); + + // Create a list of replicas with repetitions according to the weights + // and shuffle the resulting list. Any missing weights in the list + // default to 1 + pool_weights.resize(read_only_pools.len() + 1, 1); + let mut replica_order: Vec<_> = pool_weights + .iter() + .enumerate() + .map(|(i, weight)| { + let replica = if i == 0 { + ReplicaId::Main + } else { + ReplicaId::ReadOnly(i - 1) + }; + vec![replica; *weight] + }) + .flatten() + .collect(); + let mut rng = thread_rng(); + replica_order.shuffle(&mut rng); + debug!(logger, "Using postgres host order {:?}", replica_order); + + // Create the store + let store = StoreInner { + logger: logger.clone(), + conn: pool, + read_only_pools, + replica_order, + conn_round_robin_counter: AtomicUsize::new(0), + subgraph_cache: Mutex::new(LruCache::with_capacity(100)), + layout_cache: e::make_layout_cache(), + registry, + }; + let store = DeploymentStore(Arc::new(store)); + + // Return the store + store + } + + pub(crate) fn create_deployment( + &self, + schema: &Schema, + deployment: SubgraphDeploymentEntity, + site: &Site, + graft_site: Option, + replace: bool, + ) -> Result { + let conn = self.get_conn()?; + // This is a bit of a Frankenconnection: we don't have the actual + // layout yet; but for applying metadata, it's fine to use the metadata + // layout + let econn = e::Connection::new( + conn.into(), + METADATA_LAYOUT.clone(), + site.deployment.clone(), + ); + econn.transaction(|| -> Result<_, StoreError> { + let exists = deployment::exists(&econn.conn, &site.deployment)?; + + let event = if replace || !exists { + let ops = deployment.create_operations(&site.deployment); + self.apply_metadata_operations_with_conn(&econn, ops)? + } else { + StoreEvent::new(vec![]) + }; + + if !exists { + econn.create_schema(site.namespace.clone(), schema, graft_site)?; + } + Ok(event) + }) + } + + // Remove the data and metadata for the deployment `site`. This operation + // is not reversible + pub(crate) fn drop_deployment(&self, site: &Site) -> Result<(), StoreError> { + let conn = self.get_conn()?; + conn.transaction(|| e::Connection::drop_deployment(&conn, site)) + } + + /// Gets an entity from Postgres. + fn get_entity( + &self, + conn: &e::Connection, + key: &EntityKey, + ) -> Result, QueryExecutionError> { + // We should really have callers pass in a block number; but until + // that is fully plumbed in, we just use the biggest possible block + // number so that we will always return the latest version, + // i.e., the one with an infinite upper bound + conn.find(key, BLOCK_NUMBER_MAX).map_err(|e| { + QueryExecutionError::ResolveEntityError( + key.subgraph_id.clone(), + key.entity_type.to_string(), + key.entity_id.clone(), + format!("Invalid entity {}", e), + ) + }) + } + + pub(crate) fn execute_query( + &self, + conn: &e::Connection, + query: EntityQuery, + ) -> Result, QueryExecutionError> { + // Process results; deserialize JSON data + let logger = query.logger.unwrap_or(self.logger.clone()); + conn.query( + &logger, + query.collection, + query.filter, + query.order, + query.range, + query.block, + query.query_id, + ) + } + + fn check_interface_entity_uniqueness( + &self, + conn: &e::Connection, + key: &EntityKey, + ) -> Result<(), StoreError> { + // Collect all types that share an interface implementation with this + // entity type, and make sure there are no conflicting IDs. + // + // To understand why this is necessary, suppose that `Dog` and `Cat` are + // types and both implement an interface `Pet`, and both have instances + // with `id: "Fred"`. If a type `PetOwner` has a field `pets: [Pet]` + // then with the value `pets: ["Fred"]`, there's no way to disambiguate + // if that's Fred the Dog, Fred the Cat or both. + // + // This assumes that there are no concurrent writes to a subgraph. + let schema = self + .subgraph_info_with_conn(&conn.conn, &key.subgraph_id)? + .api; + let types_for_interface = schema.types_for_interface(); + let entity_type = match &key.entity_type { + EntityType::Data(s) => s, + EntityType::Metadata(_) => { + // Metadata has no interfaces + return Ok(()); + } + }; + let types_with_shared_interface = Vec::from_iter( + schema + .interfaces_for_type(entity_type) + .into_iter() + .flatten() + .map(|interface| &types_for_interface[&interface.name]) + .flatten() + .map(|object_type| &object_type.name) + .filter(|type_name| *type_name != entity_type), + ); + + if !types_with_shared_interface.is_empty() { + if let Some(conflicting_entity) = + conn.conflicting_entity(&key.entity_id, types_with_shared_interface)? + { + return Err(StoreError::ConflictingId( + entity_type.clone(), + key.entity_id.clone(), + conflicting_entity, + )); + } + } + Ok(()) + } + + /// Apply a metadata operation in Postgres. + fn apply_metadata_operation( + &self, + conn: &e::Connection, + operation: MetadataOperation, + ) -> Result { + match operation { + MetadataOperation::Set { key, data } => { + let key = key.into(); + + // Load the entity if exists + let entity = self.get_entity(conn, &key)?; + + // Identify whether this is an insert or an update operation and + // merge the changes into the entity. + let result = match entity { + Some(mut entity) => { + entity.merge_remove_null_fields(data); + conn.update(&key, entity, None).map(|_| 0) + } + None => { + // Merge with a new entity since that removes values that + // were set to Value::Null + let mut entity = Entity::new(); + entity.merge_remove_null_fields(data); + conn.insert(&key, entity, None).map(|_| 1) + } + }; + + result.map_err(|e| { + anyhow!( + "Failed to set entity ({}, {}, {}): {}", + key.subgraph_id, + key.entity_type, + key.entity_id, + e + ) + .into() + }) + } + MetadataOperation::Remove { .. } => unreachable!("metadata is never deleted"), + } + } + + fn apply_entity_modifications( + &self, + conn: &e::Connection, + mods: Vec, + ptr: Option<&EthereumBlockPointer>, + stopwatch: StopwatchMetrics, + ) -> Result<(), StoreError> { + let mut count = 0; + + for modification in mods { + use EntityModification::*; + + let do_count = modification.entity_key().entity_type.is_data_type(); + let n = match modification { + Overwrite { key, data } => { + let section = stopwatch.start_section("check_interface_entity_uniqueness"); + self.check_interface_entity_uniqueness(conn, &key)?; + section.end(); + + let _section = stopwatch.start_section("apply_entity_modifications_update"); + conn.update(&key, data, ptr).map(|_| 0) + } + Insert { key, data } => { + let section = stopwatch.start_section("check_interface_entity_uniqueness"); + self.check_interface_entity_uniqueness(conn, &key)?; + section.end(); + + let _section = stopwatch.start_section("apply_entity_modifications_insert"); + conn.insert(&key, data, ptr).map(|_| 1) + } + Remove { key } => conn + .delete(&key, ptr) + // This conversion is ok since n will only be 0 or 1 + .map(|n| -(n as i32)) + .map_err(|e| { + anyhow!( + "Failed to remove entity ({}, {}, {}): {}", + key.subgraph_id, + key.entity_type, + key.entity_id, + e + ) + .into() + }), + }?; + if do_count { + count += n; + } + } + conn.update_entity_count(count)?; + Ok(()) + } + + pub(crate) fn apply_metadata_operations_with_conn( + &self, + econn: &e::Connection, + operations: Vec, + ) -> Result { + // Emit a store event for the changes we are about to make + let event: StoreEvent = operations.clone().into(); + + // Actually apply the operations + for operation in operations.into_iter() { + self.apply_metadata_operation(econn, operation)?; + } + Ok(event) + } + + /// Execute a closure with a connection to the database. + /// + /// # API + /// The API of using a closure to bound the usage of the connection serves several + /// purposes: + /// + /// * Moves blocking database access out of the `Future::poll`. Within + /// `Future::poll` (which includes all `async` methods) it is illegal to + /// perform a blocking operation. This includes all accesses to the + /// database, acquiring of locks, etc. Calling a blocking operation can + /// cause problems with `Future` combinators (including but not limited + /// to select, timeout, and FuturesUnordered) and problems with + /// executors/runtimes. This method moves the database work onto another + /// thread in a way which does not block `Future::poll`. + /// + /// * Limit the total number of connections. Because the supplied closure + /// takes a reference, we know the scope of the usage of all entity + /// connections and can limit their use in a non-blocking way. + /// + /// # Cancellation + /// The normal pattern for futures in Rust is drop to cancel. Once we + /// spawn the database work in a thread though, this expectation no longer + /// holds because the spawned task is the independent of this future. So, + /// this method provides a cancel token which indicates that the `Future` + /// has been dropped. This isn't *quite* as good as drop on cancel, + /// because a drop on cancel can do things like cancel http requests that + /// are in flight, but checking for cancel periodically is a significant + /// improvement. + /// + /// The implementation of the supplied closure should check for cancel + /// between every operation that is potentially blocking. This includes + /// any method which may interact with the database. The check can be + /// conveniently written as `token.check_cancel()?;`. It is low overhead + /// to check for cancel, so when in doubt it is better to have too many + /// checks than too few. + /// + /// # Panics: + /// * This task will panic if the supplied closure panics + /// * This task will panic if the supplied closure returns Err(Cancelled) + /// when the supplied cancel token is not cancelled. + pub(crate) async fn with_conn( + &self, + f: impl 'static + + Send + + FnOnce( + &PooledConnection>, + &CancelHandle, + ) -> Result>, + ) -> Result { + self.conn.with_conn(f).await + } + + /// Executes a closure with an `e::Connection` reference. + /// The `e::Connection` gives access to subgraph specific storage in the database - mostly for + /// storing and loading entities local to that subgraph. + /// + /// This uses `with_conn` under the hood. Please see it's documentation for important details + /// about usage. + async fn with_entity_conn( + self: Arc, + site: Arc, + f: impl 'static + + Send + + FnOnce(&e::Connection, &CancelHandle) -> Result>, + ) -> Result { + let store = self.cheap_clone(); + + // Duplicated logic: No need to make re-usable when the + // other end will go away. + // See also 220c1ae9-3e8a-42d3-bcc5-b1244a69b8a9 + let start = Instant::now(); + let registry = self.registry.cheap_clone(); + + self.with_conn(move |conn, cancel_handle| { + registry + .global_deployment_counter( + "deployment_get_entity_conn_secs", + "total time spent getting an entity connection", + site.deployment.as_str(), + ) + .map_err(|e| CancelableError::Error(StoreError::Unknown(e.into())))? + .inc_by(start.elapsed().as_secs_f64()); + + cancel_handle.check_cancel()?; + let layout = store.layout(&conn, &site.namespace, &site.deployment)?; + cancel_handle.check_cancel()?; + let conn = e::Connection::new(conn.into(), layout, site.deployment.clone()); + + f(&conn, cancel_handle) + }) + .await + } + + /// Deprecated. Use `with_conn` instead. + fn get_conn(&self) -> Result>, Error> { + self.conn.get_with_timeout_warning(&self.logger) + } + + /// Panics if `idx` is not a valid index for a read only pool. + fn read_only_conn( + &self, + idx: usize, + ) -> Result>, Error> { + self.read_only_pools[idx].get().map_err(Error::from) + } + + // Duplicated logic - this function may eventually go away. + // See also 220c1ae9-3e8a-42d3-bcc5-b1244a69b8a9 + /// Deprecated. Use `with_entity_conn` instead + pub(crate) fn get_entity_conn( + &self, + site: &Site, + replica: ReplicaId, + ) -> Result { + assert!(!site.namespace.is_metadata()); + + let start = Instant::now(); + let conn = match replica { + ReplicaId::Main => self.get_conn()?, + ReplicaId::ReadOnly(idx) => self.read_only_conn(idx)?, + }; + self.registry + .global_deployment_counter( + "deployment_get_entity_conn_secs", + "total time spent getting an entity connection", + site.deployment.as_str(), + )? + .inc_by(start.elapsed().as_secs_f64()); + let data = self.layout(&conn, &site.namespace, &site.deployment)?; + Ok(e::Connection::new( + conn.into(), + data, + site.deployment.clone(), + )) + } + + pub(crate) fn wait_stats(&self, replica: ReplicaId) -> &PoolWaitStats { + match replica { + ReplicaId::Main => &self.conn.wait_stats, + ReplicaId::ReadOnly(idx) => &self.read_only_pools[idx].wait_stats, + } + } + + /// Return the layout for the subgraph. Since constructing a `Layout` + /// object takes a bit of computation, we cache layout objects that do + /// not have a pending migration in the Store, i.e., for the lifetime of + /// the Store. Layout objects with a pending migration can not be + /// cached for longer than a transaction since they might change + /// without us knowing + pub(crate) fn layout( + &self, + conn: &PgConnection, + namespace: &Namespace, + subgraph: &SubgraphDeploymentId, + ) -> Result, StoreError> { + assert!(!namespace.is_metadata()); + + if let Some(layout) = self.layout_cache.lock().unwrap().get(subgraph) { + return Ok(layout.clone()); + } + + let layout = Arc::new(e::Connection::layout(conn, namespace.clone(), subgraph)?); + if layout.is_cacheable() { + &self + .layout_cache + .lock() + .unwrap() + .insert(subgraph.clone(), layout.clone()); + } + Ok(layout.clone()) + } + + fn subgraph_info_with_conn( + &self, + conn: &PgConnection, + subgraph_id: &SubgraphDeploymentId, + ) -> Result { + if let Some(info) = self.subgraph_cache.lock().unwrap().get(&subgraph_id) { + return Ok(info.clone()); + } + + let (input_schema, description, repository) = + deployment::manifest_info(&conn, subgraph_id.to_owned())?; + + let graft_block = + deployment::graft_point(&conn, &subgraph_id)?.map(|(_, ptr)| ptr.number as i32); + + let features = deployment::features(&conn, subgraph_id)?; + + // Generate an API schema for the subgraph and make sure all types in the + // API schema have a @subgraphId directive as well + let mut schema = input_schema.clone(); + schema.document = + api_schema(&schema.document, &features).map_err(|e| StoreError::Unknown(e.into()))?; + schema.add_subgraph_id_directives(subgraph_id.clone()); + + let info = SubgraphInfo { + input: Arc::new(input_schema), + api: Arc::new(ApiSchema::from_api_schema(schema)?), + graft_block, + description, + repository, + }; + + // Insert the schema into the cache. + let mut cache = self.subgraph_cache.lock().unwrap(); + cache.insert(subgraph_id.clone(), info); + + Ok(cache.get(&subgraph_id).unwrap().clone()) + } + + pub(crate) fn subgraph_info( + &self, + subgraph_id: &SubgraphDeploymentId, + ) -> Result { + if let Some(info) = self.subgraph_cache.lock().unwrap().get(&subgraph_id) { + return Ok(info.clone()); + } + + let conn = self.get_conn()?; + self.subgraph_info_with_conn(&conn, subgraph_id) + } + + fn block_ptr_with_conn( + subgraph_id: &SubgraphDeploymentId, + conn: &e::Connection, + ) -> Result, Error> { + Ok(deployment::block_ptr(&conn.conn, subgraph_id)?) + } + + pub(crate) fn deployment_details( + &self, + ids: Vec, + ) -> Result, StoreError> { + let conn = self.get_conn()?; + conn.transaction(|| -> Result<_, StoreError> { detail::deployment_details(&conn, ids) }) + } + + pub(crate) fn deployment_statuses( + &self, + sites: &Vec>, + ) -> Result, StoreError> { + let conn = self.get_conn()?; + conn.transaction(|| -> Result, StoreError> { + detail::deployment_statuses(&conn, sites) + }) + } + + pub(crate) fn deployment_exists_and_synced( + &self, + id: &SubgraphDeploymentId, + ) -> Result { + let conn = self.get_conn()?; + deployment::exists_and_synced(&conn, id.as_str()) + } + + pub(crate) fn deployment_synced(&self, id: &SubgraphDeploymentId) -> Result<(), StoreError> { + let conn = self.get_conn()?; + conn.transaction(|| deployment::set_synced(&conn, id)) + } + + // Only used for tests + #[cfg(debug_assertions)] + pub(crate) fn drop_deployment_schema(&self, namespace: &Namespace) -> Result<(), StoreError> { + let conn = self.get_conn()?; + deployment::drop_schema(&conn, namespace) + } + + // Only used for tests + #[cfg(debug_assertions)] + pub(crate) fn drop_all_metadata(&self) -> Result<(), StoreError> { + // Delete metadata entities in each shard + // Generated by running 'layout -g delete subgraphs.graphql' + const QUERY: &str = " + delete from subgraphs.ethereum_block_handler_filter_entity; + delete from subgraphs.ethereum_contract_source; + delete from subgraphs.dynamic_ethereum_contract_data_source; + delete from subgraphs.ethereum_contract_abi; + delete from subgraphs.subgraph; + delete from subgraphs.subgraph_deployment; + delete from subgraphs.ethereum_block_handler_entity; + delete from subgraphs.subgraph_deployment_assignment; + delete from subgraphs.ethereum_contract_mapping; + delete from subgraphs.subgraph_version; + delete from subgraphs.subgraph_manifest; + delete from subgraphs.ethereum_call_handler_entity; + delete from subgraphs.ethereum_contract_data_source; + delete from subgraphs.ethereum_contract_data_source_template; + delete from subgraphs.ethereum_contract_data_source_template_source; + delete from subgraphs.ethereum_contract_event_handler; + "; + + let conn = self.get_conn()?; + conn.batch_execute(QUERY)?; + conn.batch_execute("delete from deployment_schemas;")?; + Ok(()) + } +} + +/// Methods that back the trait `graph::components::Store`, but have small +/// variations in their signatures +impl DeploymentStore { + pub(crate) fn block_ptr(&self, site: &Site) -> Result, Error> { + Self::block_ptr_with_conn( + &site.deployment, + &self + .get_entity_conn(site, ReplicaId::Main) + .map_err(|e| QueryExecutionError::StoreError(e.into()))?, + ) + } + + pub(crate) fn supports_proof_of_indexing<'a>( + self: Arc, + site: Arc, + ) -> DynTryFuture<'a, bool> { + async move { + self.with_entity_conn(site, |conn, cancel| { + cancel.check_cancel()?; + Ok(conn.supports_proof_of_indexing()) + }) + .await + .map_err(|e| e.into()) + } + .boxed() + } + + pub(crate) fn get_proof_of_indexing<'a>( + self: Arc, + site: Arc, + indexer: &'a Option
, + block: EthereumBlockPointer, + ) -> DynTryFuture<'a, Option<[u8; 32]>> { + let logger = self.logger.cheap_clone(); + let indexer = indexer.clone(); + let site2 = site.clone(); + let site3 = site.clone(); + + async move { + let entities = self + .with_entity_conn(site2, move |conn, cancel| { + cancel.check_cancel()?; + + if !conn.supports_proof_of_indexing() { + return Ok(None); + } + + conn.transaction::<_, CancelableError, _>(move || { + let latest_block_ptr = + match Self::block_ptr_with_conn(&site.deployment, conn)? { + Some(inner) => inner, + None => return Ok(None), + }; + + cancel.check_cancel()?; + + // FIXME: (Determinism) + // + // It is vital to ensure that the block hash given in the query + // is a parent of the latest block indexed for the subgraph. + // Unfortunately the machinery needed to do this is not yet in place. + // The best we can do right now is just to make sure that the block number + // is high enough. + if latest_block_ptr.number < block.number { + return Ok(None); + } + + let entities = conn + .query::( + &logger, + EntityCollection::All(vec![POI_OBJECT.to_owned()]), + None, + EntityOrder::Default, + EntityRange { + first: None, + skip: 0, + }, + block.number.try_into().unwrap(), + None, + ) + .map_err(anyhow::Error::from)?; + + Ok(Some(entities)) + }) + .map_err(|e| e.into()) + }) + .await?; + + let entities = if let Some(entities) = entities { + entities + } else { + return Ok(None); + }; + + let mut by_causality_region = entities + .into_iter() + .map(|e| { + let causality_region = e.id()?; + let digest = match e.get("digest") { + Some(Value::Bytes(b)) => Ok(b.to_owned()), + other => Err(anyhow::anyhow!( + "Entity has non-bytes digest attribute: {:?}", + other + )), + }?; + + Ok((causality_region, digest)) + }) + .collect::, anyhow::Error>>()?; + + let mut finisher = ProofOfIndexingFinisher::new(&block, &site3.deployment, &indexer); + for (name, region) in by_causality_region.drain() { + finisher.add_causality_region(&name, ®ion); + } + + Ok(Some(finisher.finish())) + } + .boxed() + } + + pub(crate) fn get( + &self, + site: &Site, + key: EntityKey, + ) -> Result, QueryExecutionError> { + let conn = self + .get_entity_conn(site, ReplicaId::Main) + .map_err(|e| QueryExecutionError::StoreError(e.into()))?; + self.get_entity(&conn, &key) + } + + pub(crate) fn get_many( + &self, + site: &Site, + ids_for_type: BTreeMap<&EntityType, Vec<&str>>, + ) -> Result>, StoreError> { + if ids_for_type.is_empty() { + return Ok(BTreeMap::new()); + } + let conn = self + .get_entity_conn(site, ReplicaId::Main) + .map_err(|e| QueryExecutionError::StoreError(e.into()))?; + conn.find_many(ids_for_type, BLOCK_NUMBER_MAX) + } + + pub(crate) fn find( + &self, + site: &Site, + query: EntityQuery, + ) -> Result, QueryExecutionError> { + let conn = self + .get_entity_conn(site, ReplicaId::Main) + .map_err(|e| QueryExecutionError::StoreError(e.into()))?; + self.execute_query(&conn, query) + } + + pub(crate) fn find_one( + &self, + site: &Site, + mut query: EntityQuery, + ) -> Result, QueryExecutionError> { + query.range = EntityRange::first(1); + + let conn = self + .get_entity_conn(site, ReplicaId::Main) + .map_err(|e| QueryExecutionError::StoreError(e.into()))?; + + let mut results = self.execute_query(&conn, query)?; + match results.len() { + 0 | 1 => Ok(results.pop()), + n => panic!("find_one query found {} results", n), + } + } + + pub(crate) fn transact_block_operations( + &self, + site: &Site, + block_ptr_to: EthereumBlockPointer, + mods: Vec, + stopwatch: StopwatchMetrics, + deterministic_errors: Vec, + ) -> Result { + // All operations should apply only to data or metadata for this subgraph + if mods + .iter() + .map(|modification| modification.entity_key()) + .any(|key| key.subgraph_id != site.deployment) + { + panic!( + "transact_block_operations must affect only entities \ + in the subgraph or in the subgraph of subgraphs" + ); + } + + let econn = self.get_entity_conn(site, ReplicaId::Main)?; + + let event = econn.transaction(|| -> Result<_, StoreError> { + let block_ptr_from = Self::block_ptr_with_conn(&site.deployment, &econn)?; + if let Some(ref block_ptr_from) = block_ptr_from { + if block_ptr_from.number >= block_ptr_to.number { + return Err(StoreError::DuplicateBlockProcessing( + site.deployment.clone(), + block_ptr_to.number, + )); + } + } + + // Emit a store event for the changes we are about to make. We + // wait with sending it until we have done all our other work + // so that we do not hold a lock on the notification queue + // for longer than we have to + let event: StoreEvent = mods.iter().collect(); + + // Make the changes + let section = stopwatch.start_section("apply_entity_modifications"); + self.apply_entity_modifications(&econn, mods, Some(&block_ptr_to), stopwatch)?; + section.end(); + + if !deterministic_errors.is_empty() { + deployment::insert_subgraph_errors( + &econn.conn, + &site.deployment, + deterministic_errors, + block_ptr_to.block_number(), + )?; + } + + let metadata_event = + deployment::forward_block_ptr(&econn.conn, &site.deployment, block_ptr_to)?; + Ok(event.extend(metadata_event)) + })?; + + Ok(event) + } + + pub(crate) fn revert_block_operations( + &self, + site: &Site, + block_ptr_to: EthereumBlockPointer, + ) -> Result { + let econn = self.get_entity_conn(site, ReplicaId::Main)?; + + let event = econn.transaction(|| -> Result<_, StoreError> { + // Unwrap: If we are reverting then the block ptr is not `None`. + let block_ptr_from = Self::block_ptr_with_conn(&site.deployment, &econn)?.unwrap(); + + // Sanity check on block numbers + if block_ptr_from.number != block_ptr_to.number + 1 { + panic!("revert_block_operations must revert a single block only"); + } + + // Don't revert past a graft point + let info = self.subgraph_info_with_conn(&econn.conn, &site.deployment)?; + if let Some(graft_block) = info.graft_block { + if graft_block as u64 > block_ptr_to.number { + return Err(anyhow!( + "Can not revert subgraph `{}` to block {} as it was \ + grafted at block {} and reverting past a graft point \ + is not possible", + site.deployment.clone(), + block_ptr_to.number, + graft_block + ) + .into()); + } + } + + let metadata_event = + deployment::revert_block_ptr(&econn.conn, &site.deployment, block_ptr_to)?; + + let (event, count) = econn.revert_block(&block_ptr_from)?; + econn.update_entity_count(count)?; + Ok(event.extend(metadata_event)) + })?; + + Ok(event) + } + + pub(crate) async fn deployment_state_from_id( + &self, + id: SubgraphDeploymentId, + ) -> Result { + self.with_conn(|conn, _| deployment::state(&conn, id).map_err(|e| e.into())) + .await + } + + pub(crate) async fn fail_subgraph( + &self, + id: SubgraphDeploymentId, + error: SubgraphError, + ) -> Result<(), StoreError> { + self.with_conn(move |conn, _| { + conn.transaction(|| deployment::fail(&conn, &id, error)) + .map_err(|e| e.into()) + }) + .await?; + Ok(()) + } + + pub(crate) fn replica_for_query( + &self, + for_subscription: bool, + ) -> Result { + use std::sync::atomic::Ordering; + + let replica_id = match for_subscription { + // Pick a weighted ReplicaId. `replica_order` contains a list of + // replicas with repetitions according to their weight + false => { + let weights_count = self.replica_order.len(); + let index = + self.conn_round_robin_counter.fetch_add(1, Ordering::SeqCst) % weights_count; + *self.replica_order.get(index).unwrap() + } + // Subscriptions always go to the main replica. + true => ReplicaId::Main, + }; + + Ok(replica_id) + } + + pub(crate) async fn load_dynamic_data_sources( + &self, + id: SubgraphDeploymentId, + ) -> Result, StoreError> { + self.with_conn(move |conn, _| { + conn.transaction(|| crate::dynds::load(&conn, id.as_str())) + .map_err(|e| e.into()) + }) + .await + } + + pub(crate) fn exists_and_synced(&self, id: &SubgraphDeploymentId) -> Result { + let conn = self.get_conn()?; + conn.transaction(|| deployment::exists_and_synced(&conn, id)) + } + + pub(crate) fn graft_pending( + &self, + id: &SubgraphDeploymentId, + ) -> Result, StoreError> { + let conn = self.get_conn()?; + deployment::graft_pending(&conn, id) + } + + pub(crate) fn start_subgraph( + &self, + logger: &Logger, + site: Arc, + graft_base: Option<(Site, EthereumBlockPointer)>, + ) -> Result<(), StoreError> { + let econn = self.get_entity_conn(&site, ReplicaId::Main)?; + econn.transaction(|| econn.start_subgraph(logger, graft_base)) + } + + pub(crate) fn unfail(&self, site: Arc) -> Result<(), StoreError> { + let econn = self.get_entity_conn(&site, ReplicaId::Main)?; + econn.transaction(|| deployment::unfail(&econn.conn, &site.deployment)) + } + + #[cfg(debug_assertions)] + pub fn error_count(&self, id: &SubgraphDeploymentId) -> Result { + let conn = self.get_conn()?; + deployment::error_count(&conn, id) + } +} diff --git a/store/postgres/src/detail.rs b/store/postgres/src/detail.rs index cebf8dd2b3b..d49035a9684 100644 --- a/store/postgres/src/detail.rs +++ b/store/postgres/src/detail.rs @@ -10,68 +10,43 @@ use graph::{ bigdecimal::ToPrimitive, BigDecimal, EthereumBlockPointer, StoreError, SubgraphDeploymentId, }, }; -use graph::{ - data::subgraph::{schema::SubgraphHealth, status}, - prelude::web3::types::H256, -}; -use std::ops::Bound; -use std::{convert::TryFrom, str::FromStr}; +use graph::{data::subgraph::status, prelude::web3::types::H256}; +use std::convert::TryFrom; +use std::{ops::Bound, sync::Arc}; -// This is not a real table, only a view. We can use diesel to read from it -// but write attempts will fail -table! { - subgraphs.subgraph_deployment_detail (vid) { - vid -> BigInt, - id -> Text, - manifest -> Text, - failed -> Bool, - health -> Text, - synced -> Bool, - fatal_error -> Nullable, - non_fatal_errors -> Array, - earliest_ethereum_block_hash -> Nullable, - earliest_ethereum_block_number -> Nullable, - latest_ethereum_block_hash -> Nullable, - latest_ethereum_block_number -> Nullable, - entity_count -> Numeric, - graft_base -> Nullable, - graft_block_hash -> Nullable, - graft_block_number -> Nullable, - network -> Text, - // We don't map block_range - // block_range -> Range, - } -} - -use crate::deployment::subgraph_error; - -allow_tables_to_appear_in_same_query!(subgraph_deployment_detail, subgraph_error); +use crate::deployment::{subgraph_deployment, subgraph_error, SubgraphHealth as HealthType}; +use crate::primary::Site; type Bytes = Vec; #[derive(Queryable, QueryableByName)] -#[table_name = "subgraph_deployment_detail"] +#[table_name = "subgraph_deployment"] // We map all fields to make loading `Detail` with diesel easier, but we // don't need all the fields #[allow(dead_code)] pub struct DeploymentDetail { - pub vid: i64, + vid: i64, pub id: String, - pub manifest: String, + manifest: String, pub failed: bool, - pub health: String, + health: HealthType, pub synced: bool, - pub fatal_error: Option, - pub non_fatal_errors: Vec, - pub earliest_ethereum_block_hash: Option, - pub earliest_ethereum_block_number: Option, + fatal_error: Option, + non_fatal_errors: Vec, + earliest_ethereum_block_hash: Option, + earliest_ethereum_block_number: Option, pub latest_ethereum_block_hash: Option, pub latest_ethereum_block_number: Option, + last_healthy_ethereum_block_hash: Option, + last_healthy_ethereum_block_number: Option, pub entity_count: BigDecimal, - pub graft_base: Option, - pub graft_block_hash: Option, - pub graft_block_number: Option, - pub network: String, + graft_base: Option, + graft_block_hash: Option, + graft_block_number: Option, + reorg_count: i32, + current_reorg_depth: i32, + max_reorg_depth: i32, + block_range: (Bound, Bound), } #[derive(Queryable, QueryableByName)] @@ -90,7 +65,7 @@ struct ErrorDetail { block_range: (Bound, Bound), } -struct DetailAndError(DeploymentDetail, Option); +struct DetailAndError<'a>(DeploymentDetail, Option, &'a Vec>); pub(crate) fn block( id: &str, @@ -161,11 +136,11 @@ impl TryFrom for SubgraphError { } } -impl TryFrom for status::Info { +impl<'a> TryFrom> for status::Info { type Error = StoreError; fn try_from(detail_and_error: DetailAndError) -> Result { - let DetailAndError(detail, error) = detail_and_error; + let DetailAndError(detail, error, sites) = detail_and_error; let DeploymentDetail { vid: _, @@ -184,9 +159,14 @@ impl TryFrom for status::Info { graft_base: _, graft_block_hash: _, graft_block_number: _, - network, + .. } = detail; + let site = sites + .iter() + .find(|site| site.deployment.as_str() == &id) + .ok_or_else(|| constraint_violation!("missing site for subgraph `{}`", id))?; + // This needs to be filled in later since it lives in a // different shard let chain_head_block = None; @@ -202,9 +182,9 @@ impl TryFrom for status::Info { latest_ethereum_block_hash, latest_ethereum_block_number, )?; - let health = SubgraphHealth::from_str(&health)?; + let health = health.into(); let chain = status::ChainInfo { - network, + network: site.network.clone(), chain_head_block, earliest_block, latest_block, @@ -232,7 +212,7 @@ pub(crate) fn deployment_details( conn: &PgConnection, deployments: Vec, ) -> Result, StoreError> { - use subgraph_deployment_detail as d; + use subgraph_deployment as d; // Empty deployments means 'all of them' let details = if deployments.is_empty() { @@ -247,26 +227,31 @@ pub(crate) fn deployment_details( pub(crate) fn deployment_statuses( conn: &PgConnection, - deployments: Vec, + sites: &Vec>, ) -> Result, StoreError> { - use subgraph_deployment_detail as d; + use subgraph_deployment as d; use subgraph_error as e; // Empty deployments means 'all of them' - if deployments.is_empty() { + if sites.is_empty() { d::table .left_outer_join(e::table.on(d::fatal_error.eq(e::id.nullable()))) .load::<(DeploymentDetail, Option)>(conn)? .into_iter() - .map(|(detail, error)| status::Info::try_from(DetailAndError(detail, error))) + .map(|(detail, error)| status::Info::try_from(DetailAndError(detail, error, sites))) .collect() } else { + let ids: Vec<_> = sites + .into_iter() + .map(|site| site.deployment.to_string()) + .collect(); + d::table .left_outer_join(e::table.on(d::fatal_error.eq(e::id.nullable()))) - .filter(d::id.eq_any(&deployments)) + .filter(d::id.eq_any(&ids)) .load::<(DeploymentDetail, Option)>(conn)? .into_iter() - .map(|(detail, error)| status::Info::try_from(DetailAndError(detail, error))) + .map(|(detail, error)| status::Info::try_from(DetailAndError(detail, error, sites))) .collect() } } diff --git a/store/postgres/src/functions.rs b/store/postgres/src/functions.rs index 72c261f160a..d7a20b86e6d 100644 --- a/store/postgres/src/functions.rs +++ b/store/postgres/src/functions.rs @@ -1,4 +1,4 @@ -use diesel::sql_types::{BigInt, Bool, Text}; +use diesel::sql_types::{Bool, Text}; // Create modules for hosting stored procedures sql_function! { fn current_setting(setting_name: Text, missing_ok: Bool) } @@ -7,14 +7,6 @@ sql_function! { fn set_config(setting_name: Text, new_value: Text, is_local: Bool) } -sql_function! { - fn attempt_chain_head_update(net_name: Text, ancestor_count: BigInt) -> Array -} - -sql_function! { - fn lookup_ancestor_block(start_block_hash: Text, ancestor_count: BigInt) -> Nullable -} - sql_function! { fn pg_notify(channel: Text, msg: Text) } diff --git a/store/postgres/src/lib.rs b/store/postgres/src/lib.rs index 9cecee16dd9..dfca3c986f8 100644 --- a/store/postgres/src/lib.rs +++ b/store/postgres/src/lib.rs @@ -1,3 +1,7 @@ +//! This crate implements the store for all chain and subgraph data. See the +//! [Store] for the details of how the store is organized across +//! different databases/shards. + #[macro_use] extern crate derive_more; #[macro_use] @@ -20,50 +24,46 @@ extern crate serde; extern crate uuid; mod block_range; +mod block_store; mod catalog; mod chain_head_listener; mod chain_store; pub mod connection_pool; -mod db_schema; mod deployment; +mod deployment_store; mod detail; mod dynds; mod entities; mod functions; mod jsonb; -mod network_store; mod notification_listener; mod primary; pub mod query_store; mod relational; mod relational_queries; -mod sharded_store; mod sql_value; -pub mod store; +mod store; mod store_events; - -#[cfg(debug_assertions)] -pub mod db_schema_for_tests { - pub use crate::db_schema::ethereum_blocks; - pub use crate::db_schema::ethereum_networks; -} +mod subgraph_store; #[cfg(debug_assertions)] pub mod layout_for_tests { pub use crate::block_range::*; + pub use crate::block_store::FAKE_NETWORK_SHARED; + pub use crate::chain_store::test_support as chain_support; pub use crate::entities::STRING_PREFIX_SIZE; pub use crate::primary::{Connection, Namespace, EVENT_TAP, EVENT_TAP_ENABLED}; pub use crate::relational::*; } +pub use self::block_store::BlockStore; pub use self::chain_head_listener::ChainHeadUpdateListener; pub use self::chain_store::ChainStore; pub use self::detail::DeploymentDetail; -pub use self::network_store::NetworkStore; pub use self::primary::UnusedDeployment; -pub use self::sharded_store::{unused, DeploymentPlacer, Shard, ShardedStore, PRIMARY_SHARD}; -pub use self::store::{Store, StoreConfig}; +pub use self::store::Store; pub use self::store_events::SubscriptionManager; +pub use self::subgraph_store::{unused, DeploymentPlacer, Shard, SubgraphStore, PRIMARY_SHARD}; /// This module is only meant to support command line tooling. It must not /// be used in 'normal' graph-node code @@ -71,7 +71,8 @@ pub mod command_support { pub mod catalog { pub use crate::primary::Connection; pub use crate::primary::{ - deployment_schemas, subgraph, subgraph_deployment_assignment, subgraph_version, + deployment_schemas, ens_names, subgraph, subgraph_deployment_assignment, + subgraph_version, }; } pub use crate::entities::Connection; diff --git a/store/postgres/src/network_store.rs b/store/postgres/src/network_store.rs deleted file mode 100644 index 2a74324379b..00000000000 --- a/store/postgres/src/network_store.rs +++ /dev/null @@ -1,378 +0,0 @@ -use std::sync::Arc; - -use graph::{ - components::{ - server::index_node::VersionInfo, - store::{EntityType, QueryStoreManager, StoredDynamicDataSource}, - }, - data::subgraph::schema::SubgraphError, - data::subgraph::status, - prelude::{ - ethabi, - web3::types::{Address, H256}, - BlockNumber, ChainHeadUpdateStream, ChainStore as ChainStoreTrait, CheapClone, Error, - EthereumBlock, EthereumBlockPointer, EthereumCallCache, Future, LightEthereumBlock, NodeId, - Schema, Store as StoreTrait, StoreError, Stream, SubgraphDeploymentEntity, - SubgraphDeploymentId, SubgraphName, SubgraphVersionSwitchingMode, - }, -}; - -use crate::{chain_store::ChainStore, query_store::QueryStore, ShardedStore}; - -pub struct NetworkStore { - store: Arc, - chain_store: Arc, -} - -impl NetworkStore { - pub fn new(store: Arc, chain_store: Arc) -> Self { - Self { store, chain_store } - } - - pub fn store(&self) -> Arc { - self.store.cheap_clone() - } - - // Only for tests to simplify their handling of test fixtures, so that - // tests can reset the block pointer of a subgraph by recreating it - #[cfg(debug_assertions)] - pub fn create_deployment_replace( - &self, - name: SubgraphName, - schema: &Schema, - deployment: SubgraphDeploymentEntity, - node_id: NodeId, - network_name: String, - mode: SubgraphVersionSwitchingMode, - ) -> Result<(), StoreError> { - self.store - .create_deployment_replace(name, schema, deployment, node_id, network_name, mode) - } - - #[cfg(debug_assertions)] - pub fn delete_all_entities_for_test_use_only(&self) -> Result<(), StoreError> { - self.store.delete_all_entities_for_test_use_only() - } -} - -#[async_trait::async_trait] -impl StoreTrait for NetworkStore { - fn block_ptr( - &self, - subgraph_id: &graph::prelude::SubgraphDeploymentId, - ) -> Result, Error> { - self.store.block_ptr(subgraph_id) - } - - fn supports_proof_of_indexing<'a>( - self: Arc, - subgraph_id: &'a graph::prelude::SubgraphDeploymentId, - ) -> graph::prelude::DynTryFuture<'a, bool> { - self.store.clone().supports_proof_of_indexing(subgraph_id) - } - - fn get_proof_of_indexing<'a>( - self: Arc, - subgraph_id: &'a graph::prelude::SubgraphDeploymentId, - indexer: &'a Option
, - block: EthereumBlockPointer, - ) -> graph::prelude::DynTryFuture<'a, Option<[u8; 32]>> { - self.store - .clone() - .get_proof_of_indexing(subgraph_id, indexer, block) - } - - fn get( - &self, - key: graph::prelude::EntityKey, - ) -> Result, graph::prelude::QueryExecutionError> { - self.store.get(key) - } - - fn get_many( - &self, - subgraph_id: &graph::prelude::SubgraphDeploymentId, - ids_for_type: std::collections::BTreeMap<&EntityType, Vec<&str>>, - ) -> Result< - std::collections::BTreeMap>, - graph::prelude::StoreError, - > { - self.store.get_many(subgraph_id, ids_for_type) - } - - fn find( - &self, - query: graph::prelude::EntityQuery, - ) -> Result, graph::prelude::QueryExecutionError> { - self.store.find(query) - } - - fn find_one( - &self, - query: graph::prelude::EntityQuery, - ) -> Result, graph::prelude::QueryExecutionError> { - self.store.find_one(query) - } - - fn find_ens_name( - &self, - hash: &str, - ) -> Result, graph::prelude::QueryExecutionError> { - self.store.find_ens_name(hash) - } - - fn transact_block_operations( - &self, - subgraph_id: graph::prelude::SubgraphDeploymentId, - block_ptr_to: EthereumBlockPointer, - mods: Vec, - stopwatch: graph::prelude::StopwatchMetrics, - deterministic_errors: Vec, - ) -> Result<(), graph::prelude::StoreError> { - self.store.transact_block_operations( - subgraph_id, - block_ptr_to, - mods, - stopwatch, - deterministic_errors, - ) - } - - fn revert_block_operations( - &self, - subgraph_id: graph::prelude::SubgraphDeploymentId, - block_ptr_to: EthereumBlockPointer, - ) -> Result<(), graph::prelude::StoreError> { - self.store - .revert_block_operations(subgraph_id, block_ptr_to) - } - - fn deployment_state_from_name( - &self, - name: graph::prelude::SubgraphName, - ) -> Result { - self.store.deployment_state_from_name(name) - } - - fn deployment_state_from_id( - &self, - id: graph::prelude::SubgraphDeploymentId, - ) -> Result { - self.store.deployment_state_from_id(id) - } - - async fn fail_subgraph( - &self, - id: SubgraphDeploymentId, - error: SubgraphError, - ) -> Result<(), StoreError> { - self.store.fail_subgraph(id, error).await - } - - fn create_subgraph_deployment( - &self, - name: SubgraphName, - schema: &Schema, - deployment: SubgraphDeploymentEntity, - node_id: NodeId, - network_name: String, - mode: SubgraphVersionSwitchingMode, - ) -> Result<(), StoreError> { - self.store - .create_subgraph_deployment(name, schema, deployment, node_id, network_name, mode) - } - - fn start_subgraph_deployment( - &self, - logger: &graph::prelude::Logger, - subgraph_id: &graph::prelude::SubgraphDeploymentId, - ) -> Result<(), graph::prelude::StoreError> { - self.store.start_subgraph_deployment(logger, subgraph_id) - } - - fn is_deployment_synced(&self, id: &SubgraphDeploymentId) -> Result { - self.store.is_deployment_synced(id) - } - - fn deployment_synced(&self, id: &graph::prelude::SubgraphDeploymentId) -> Result<(), Error> { - self.store.deployment_synced(id) - } - - fn remove_subgraph(&self, name: SubgraphName) -> Result<(), StoreError> { - self.store.remove_subgraph(name) - } - - fn reassign_subgraph( - &self, - id: &SubgraphDeploymentId, - node: &NodeId, - ) -> Result<(), StoreError> { - self.store.reassign_subgraph(id, node) - } - - fn unassign_subgraph(&self, id: &SubgraphDeploymentId) -> Result<(), StoreError> { - self.store.unassign_subgraph(id) - } - - fn create_subgraph(&self, name: SubgraphName) -> Result { - self.store.create_subgraph(name) - } - - fn status(&self, filter: status::Filter) -> Result, StoreError> { - self.store.status(filter) - } - - async fn load_dynamic_data_sources( - &self, - subgraph_id: SubgraphDeploymentId, - ) -> Result, StoreError> { - self.store.load_dynamic_data_sources(subgraph_id).await - } - - fn assigned_node(&self, id: &SubgraphDeploymentId) -> Result, StoreError> { - self.store.assigned_node(id) - } - - fn assignments(&self, node: &NodeId) -> Result, StoreError> { - self.store.assignments(node) - } - - fn subgraph_exists(&self, name: &SubgraphName) -> Result { - self.store.subgraph_exists(name) - } - - fn input_schema( - &self, - subgraph_id: &graph::prelude::SubgraphDeploymentId, - ) -> Result, StoreError> { - self.store.input_schema(subgraph_id) - } - - fn api_schema( - &self, - subgraph_id: &graph::prelude::SubgraphDeploymentId, - ) -> Result, StoreError> { - self.store.api_schema(subgraph_id) - } - - fn network_name( - &self, - subgraph_id: &graph::prelude::SubgraphDeploymentId, - ) -> Result, StoreError> { - self.store.network_name(subgraph_id) - } - - fn version_info(&self, version_id: &str) -> Result { - self.store.version_info(version_id) - } - - fn versions_for_subgraph_id( - &self, - subgraph_id: &str, - ) -> Result<(Option, Option), StoreError> { - self.store.versions_for_subgraph_id(subgraph_id) - } -} - -impl QueryStoreManager for NetworkStore { - fn query_store( - &self, - target: graph::data::query::QueryTarget, - for_subscription: bool, - ) -> Result< - Arc, - graph::prelude::QueryExecutionError, - > { - let (store, site, replica) = self.store.replica_for_query(target, for_subscription)?; - Ok(Arc::new(QueryStore::new( - store, - self.chain_store.clone(), - site, - replica, - ))) - } -} - -impl EthereumCallCache for NetworkStore { - fn get_call( - &self, - contract_address: ethabi::Address, - encoded_call: &[u8], - block: EthereumBlockPointer, - ) -> Result>, Error> { - self.store.get_call(contract_address, encoded_call, block) - } - - fn set_call( - &self, - contract_address: ethabi::Address, - encoded_call: &[u8], - block: EthereumBlockPointer, - return_value: &[u8], - ) -> Result<(), Error> { - self.store - .set_call(contract_address, encoded_call, block, return_value) - } -} - -impl ChainStoreTrait for NetworkStore { - fn genesis_block_ptr(&self) -> Result { - self.chain_store.genesis_block_ptr() - } - - fn upsert_blocks( - &self, - blocks: B, - ) -> Box + Send + 'static> - where - B: Stream + Send + 'static, - E: From + Send + 'static, - Self: Sized, - { - self.chain_store.upsert_blocks(blocks) - } - - fn upsert_light_blocks(&self, blocks: Vec) -> Result<(), Error> { - self.chain_store.upsert_light_blocks(blocks) - } - - fn attempt_chain_head_update(&self, ancestor_count: u64) -> Result, Error> { - self.chain_store.attempt_chain_head_update(ancestor_count) - } - - fn chain_head_updates(&self) -> ChainHeadUpdateStream { - self.chain_store.chain_head_updates() - } - - fn chain_head_ptr(&self) -> Result, Error> { - self.chain_store.chain_head_ptr() - } - - fn blocks(&self, hashes: Vec) -> Result, Error> { - self.chain_store.blocks(hashes) - } - - fn ancestor_block( - &self, - block_ptr: EthereumBlockPointer, - offset: u64, - ) -> Result, Error> { - self.chain_store.ancestor_block(block_ptr, offset) - } - - fn cleanup_cached_blocks(&self, ancestor_count: u64) -> Result<(BlockNumber, usize), Error> { - self.chain_store.cleanup_cached_blocks(ancestor_count) - } - - fn block_hashes_by_block_number(&self, number: u64) -> Result, Error> { - self.chain_store.block_hashes_by_block_number(number) - } - - fn confirm_block_hash(&self, number: u64, hash: &H256) -> Result { - self.chain_store.confirm_block_hash(number, hash) - } - - fn block_number(&self, block_hash: H256) -> Result, StoreError> { - self.chain_store.block_number(block_hash) - } -} diff --git a/store/postgres/src/notification_listener.rs b/store/postgres/src/notification_listener.rs index 60333d63a17..fdf19054425 100644 --- a/store/postgres/src/notification_listener.rs +++ b/store/postgres/src/notification_listener.rs @@ -28,6 +28,7 @@ lazy_static! { .unwrap_or(Duration::from_secs(300)); } +#[derive(Clone)] /// This newtype exists to make it hard to misuse the `NotificationListener` API in a way that /// could impact security. pub struct SafeChannelName(String); @@ -45,6 +46,10 @@ impl SafeChannelName { pub fn i_promise_this_is_safe(channel_name: impl Into) -> Self { SafeChannelName(channel_name.into()) } + + pub fn as_str(&self) -> &str { + &self.0 + } } pub struct NotificationListener { @@ -209,6 +214,16 @@ impl EventProducer for NotificationListener { } } +mod public { + table! { + large_notifications(id) { + id -> Integer, + payload -> Text, + created_at -> Timestamp, + } + } +} + // A utility to send JSON notifications that may be larger than the // 8000 bytes limit for Postgres NOTIFY payloads. Large notifications // are written to the `large_notifications` table and their ID is sent @@ -286,9 +301,9 @@ impl JsonNotification { data: &serde_json::Value, conn: &PgConnection, ) -> Result<(), StoreError> { - use crate::db_schema::large_notifications::dsl::*; use diesel::ExpressionMethods; use diesel::RunQueryDsl; + use public::large_notifications::dsl::*; let msg = data.to_string(); diff --git a/store/postgres/src/primary.rs b/store/postgres/src/primary.rs index 45dbfc8f090..f11d227df0c 100644 --- a/store/postgres/src/primary.rs +++ b/store/postgres/src/primary.rs @@ -4,7 +4,10 @@ use diesel::{ data_types::PgTimestamp, dsl::{any, exists, not}, + pg::Pg, + serialize::Output, sql_types::{Array, Text}, + types::{FromSql, ToSql}, }; use diesel::{ dsl::{delete, insert_into, sql, update}, @@ -22,7 +25,6 @@ use graph::{ constraint_violation, data::subgraph::schema::MetadataType, data::subgraph::status, - prelude::EthereumBlockPointer, prelude::{ anyhow, bigdecimal::ToPrimitive, entity, lazy_static, serde_json, EntityChange, EntityChangeOperation, MetadataOperation, NodeId, StoreError, SubgraphDeploymentId, @@ -30,12 +32,13 @@ use graph::{ }, }; use graph::{data::subgraph::schema::generate_entity_id, prelude::StoreEvent}; +use maybe_owned::MaybeOwned; use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, convert::TryFrom, convert::TryInto, fmt, - iter::FromIterator, + io::Write, time::{SystemTime, UNIX_EPOCH}, }; @@ -43,7 +46,7 @@ use crate::{ block_range::UNVERSIONED_RANGE, detail::DeploymentDetail, notification_listener::JsonNotification, - sharded_store::{unused, Shard}, + subgraph_store::{unused, Shard}, }; #[cfg(debug_assertions)] @@ -95,6 +98,13 @@ table! { } } +table! { + public.ens_names(hash) { + hash -> Varchar, + name -> Varchar, + } +} + /// We used to support different layout schemes. The old 'Split' scheme /// which used JSONB layout has been removed, and we will only deal /// with relational layout. Trying to do anything with a 'Split' subgraph @@ -113,7 +123,7 @@ table! { shard -> Text, /// The subgraph layout scheme used for this subgraph version -> crate::primary::DeploymentSchemaVersionMapping, - network -> Nullable, + network -> Text, } } @@ -163,7 +173,7 @@ struct Schema { /// The version currently in use. Always `Relational`, attempts to load /// schemas from the database with `Split` produce an error version: DeploymentSchemaVersion, - pub network: Option, + pub network: String, } #[derive(Clone, Queryable, QueryableByName, Debug)] @@ -184,7 +194,8 @@ pub struct UnusedDeployment { pub synced: bool, } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, AsExpression, FromSqlRow)] +#[sql_type = "diesel::sql_types::Text"] /// A namespace (schema) in the database pub struct Namespace(String); @@ -226,6 +237,19 @@ impl fmt::Display for Namespace { } } +impl FromSql for Namespace { + fn from_sql(bytes: Option<&[u8]>) -> diesel::deserialize::Result { + let s = >::from_sql(bytes)?; + Namespace::new(s).map_err(Into::into) + } +} + +impl ToSql for Namespace { + fn to_sql(&self, out: &mut Output) -> diesel::serialize::Result { + >::to_sql(&self.0, out) + } +} + #[derive(Debug)] /// Details about a deployment and the shard in which it is stored. We need /// the database namespace for the deployment as that information is only @@ -237,6 +261,8 @@ pub struct Site { pub shard: Shard, /// The database namespace (schema) that holds the data for the deployment pub namespace: Namespace, + /// The name of the network to which this deployment belongs + pub network: String, } impl TryFrom for Site { @@ -257,17 +283,20 @@ impl TryFrom for Site { deployment, namespace, shard, + network: schema.network, }) } } /// A wrapper for a database connection that provides access to functionality /// that works only on the primary database -pub struct Connection(PooledConnection>); +pub struct Connection<'a>(MaybeOwned<'a, PooledConnection>>); -impl Connection { - pub fn new(conn: PooledConnection>) -> Self { - Self(conn) +impl<'a> Connection<'a> { + pub fn new( + conn: impl Into>>>, + ) -> Self { + Self(conn.into()) } pub(crate) fn transaction(&self, f: F) -> Result @@ -289,7 +318,7 @@ impl Connection { .inner_join(s::table.on(s::current_version.eq(v::id.nullable()))) .filter(s::name.eq(name.as_str())) .select(v::deployment) - .first::(&self.0) + .first::(self.0.as_ref()) .optional()?; match id { Some(id) => SubgraphDeploymentId::new(id) @@ -319,7 +348,7 @@ impl Connection { } Ok(diesel::sql_query(QUERY) - .load::(&self.0)? + .load::(self.0.as_ref())? .into_iter() .map(|r| { SubgraphDeploymentId::new(r.id.clone()) @@ -348,7 +377,7 @@ impl Connection { use subgraph as s; use subgraph_version as v; - let conn = &self.0; + let conn = self.0.as_ref(); // Subgraphs where we need to promote the version let pending_subgraph_versions: Vec<(String, String)> = s::table @@ -384,7 +413,7 @@ impl Connection { pub fn create_subgraph(&self, name: &SubgraphName) -> Result { use subgraph as s; - let conn = &self.0; + let conn = self.0.as_ref(); let id = generate_entity_id(); let created_at = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -428,7 +457,7 @@ impl Connection { use subgraph_version as v; use SubgraphVersionSwitchingMode::*; - let conn = &self.0; + let conn = self.0.as_ref(); let created_at = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -544,7 +573,7 @@ impl Connection { use subgraph as s; use subgraph_version as v; - let conn = &self.0; + let conn = self.0.as_ref(); // Get the id of the given subgraph. If no subgraph with the // name exists, there is nothing to do @@ -567,7 +596,7 @@ impl Connection { Ok( diesel::select(exists(s::table.filter(s::name.eq(name.as_str())))) - .get_result::(&self.0)?, + .get_result::(self.0.as_ref())?, ) } @@ -578,7 +607,7 @@ impl Connection { ) -> Result, StoreError> { use subgraph_deployment_assignment as a; - let conn = &self.0; + let conn = self.0.as_ref(); let updates = update(a::table.filter(a::id.eq(id.as_str()))) .set(a::node_id.eq(node.as_str())) .execute(conn)?; @@ -607,7 +636,7 @@ impl Connection { ) -> Result, StoreError> { use subgraph_deployment_assignment as a; - let conn = &self.0; + let conn = self.0.as_ref(); let delete_count = delete(a::table.filter(a::id.eq(id.as_str()))).execute(conn)?; match delete_count { @@ -630,12 +659,12 @@ impl Connection { &self, shard: Shard, subgraph: &SubgraphDeploymentId, - network_name: &str, + network: String, ) -> Result { use deployment_schemas as ds; use DeploymentSchemaVersion as v; - let conn = &self.0; + let conn = self.0.as_ref(); if let Some(schema) = self.find_site(subgraph)? { return Ok(schema); @@ -647,7 +676,7 @@ impl Connection { ds::subgraph.eq(subgraph.as_str()), ds::shard.eq(shard.as_str()), ds::version.eq(v::Relational), - ds::network.eq(network_name), + ds::network.eq(network.as_str()), )) .returning(ds::name) .get_results(conn)?; @@ -663,6 +692,7 @@ impl Connection { deployment: subgraph.clone(), namespace, shard, + network, }) } @@ -674,11 +704,11 @@ impl Connection { use unused_deployments as u; self.transaction(|| { - delete(v::table.filter(v::deployment.eq(id.as_str()))).execute(&self.0)?; - delete(ds::table.filter(ds::subgraph.eq(id.as_str()))).execute(&self.0)?; + delete(v::table.filter(v::deployment.eq(id.as_str()))).execute(self.0.as_ref())?; + delete(ds::table.filter(ds::subgraph.eq(id.as_str()))).execute(self.0.as_ref())?; update(u::table.filter(u::id.eq(id.as_str()))) .set(u::removed_at.eq(sql("now()"))) - .execute(&self.0)?; + .execute(self.0.as_ref())?; Ok(()) }) } @@ -686,7 +716,7 @@ impl Connection { pub fn find_site(&self, subgraph: &SubgraphDeploymentId) -> Result, StoreError> { let schema = deployment_schemas::table .filter(deployment_schemas::subgraph.eq(subgraph.to_string())) - .first::(&self.0) + .first::(self.0.as_ref()) .optional()?; if let Some(Schema { version, .. }) = schema { if matches!(version, DeploymentSchemaVersion::Split) { @@ -703,7 +733,7 @@ impl Connection { let ids: Vec<_> = ids.iter().map(|id| id.to_string()).collect(); let schemas = deployment_schemas::table .filter(deployment_schemas::subgraph.eq_any(ids)) - .load::(&self.0)?; + .load::(self.0.as_ref())?; schemas .into_iter() .map(|schema| { @@ -732,7 +762,7 @@ impl Connection { ds::table .filter(ds::name.ne("subgraphs")) - .load::(&self.0)? + .load::(self.0.as_ref())? .into_iter() .map(|schema| schema.try_into()) .collect() @@ -750,7 +780,7 @@ impl Connection { EVENT_TAP.lock().unwrap().push(event.clone()); } } - JsonNotification::send("store_events", &v, &self.0) + JsonNotification::send("store_events", &v, self.0.as_ref()) } /// Return the name of the node that has the fewest assignments out of the @@ -765,7 +795,7 @@ impl Connection { .select((a::node_id, sql("count(*)"))) .group_by(a::node_id) .order_by(sql::("count(*)")) - .load::<(String, i64)>(&self.0)?; + .load::<(String, i64)>(self.0.as_ref())?; // Any nodes without assignments will be missing from `assigned` let missing = nodes @@ -792,7 +822,7 @@ impl Connection { a::table .filter(a::id.eq(id.as_str())) .select(a::node_id) - .first::(&self.0) + .first::(self.0.as_ref()) .optional()? .map(|node| { NodeId::new(&node).map_err(|()| { @@ -808,7 +838,7 @@ impl Connection { a::table .filter(a::node_id.eq(node.as_str())) .select(a::id) - .load::(&self.0)? + .load::(self.0.as_ref())? .into_iter() .map(|id| { SubgraphDeploymentId::new(id).map_err(|id| { @@ -832,7 +862,7 @@ impl Connection { let nodes: HashMap<_, _> = a::table .filter(a::id.eq(any(ids))) .select((a::id, a::node_id)) - .load::<(String, String)>(&self.0)? + .load::<(String, String)>(self.0.as_ref())? .into_iter() .collect(); for mut info in &mut infos { @@ -841,64 +871,6 @@ impl Connection { Ok(infos) } - pub fn fill_chain_head_pointers( - &self, - mut infos: Vec, - ) -> Result, StoreError> { - use crate::db_schema::ethereum_networks as n; - let networks: Vec<_> = infos - .iter() - .map(|info| info.chains.iter().map(|chain| &chain.network)) - .flatten() - .collect::>() - .into_iter() - .collect(); - let pointers: Vec<(String, EthereumBlockPointer)> = n::table - .filter(n::name.eq(any(networks))) - .select((n::name, n::head_block_hash, n::head_block_number)) - .load::<(String, Option, Option)>(&self.0)? - .into_iter() - .filter_map(|(name, hash, number)| match (hash, number) { - (Some(hash), Some(number)) => Some((name, hash, number)), - _ => None, - }) - .map(|(name, hash, number)| { - EthereumBlockPointer::try_from((hash.as_str(), number)).map(|ptr| (name, ptr)) - }) - .collect::, _>>()?; - let pointers: HashMap<_, _> = HashMap::from_iter(pointers); - for info in &mut infos { - for chain in &mut info.chains { - chain.chain_head_block = pointers - .get(&chain.network) - .map(|ptr| ptr.to_owned().into()); - } - } - Ok(infos) - } - - pub fn chain_head_block(&self, network: &str) -> Result, StoreError> { - use crate::db_schema::ethereum_networks as n; - - let number: Option = n::table - .filter(n::name.eq(network)) - .select(n::head_block_number) - .first::>(&self.0) - .optional()? - .flatten(); - - number.map(|number| number.try_into()).transpose().map_err( - |e: std::num::TryFromIntError| { - constraint_violation!( - "head block number for {} is {:?} which does not fit into a u32: {}", - network, - number, - e.to_string() - ) - }, - ) - } - pub(crate) fn deployments_for_subgraph(&self, name: String) -> Result, StoreError> { use subgraph as s; use subgraph_version as v; @@ -908,7 +880,7 @@ impl Connection { .filter(s::name.eq(&name)) .order_by(v::created_at.asc()) .select(v::deployment) - .load(&self.0)?) + .load(self.0.as_ref())?) } pub fn subgraph_version( @@ -924,13 +896,13 @@ impl Connection { .select(v::deployment.nullable()) .inner_join(s::table.on(s::current_version.eq(v::id.nullable()))) .filter(s::name.eq(&name)) - .first::>(&self.0) + .first::>(self.0.as_ref()) } else { v::table .select(v::deployment.nullable()) .inner_join(s::table.on(s::pending_version.eq(v::id.nullable()))) .filter(s::name.eq(&name)) - .first::>(&self.0) + .first::>(self.0.as_ref()) }; Ok(deployment.optional()?.flatten()) } @@ -945,7 +917,7 @@ impl Connection { Ok(s::table .select((s::current_version.nullable(), s::pending_version.nullable())) .filter(s::name.eq(&name)) - .first::<(Option, Option)>(&self.0) + .first::<(Option, Option)>(self.0.as_ref()) .optional()? .unwrap_or((None, None))) } @@ -957,7 +929,7 @@ impl Connection { Ok(v::table .select(v::deployment) .filter(v::id.eq(name)) - .first::(&self.0) + .first::(self.0.as_ref()) .optional()?) } @@ -967,7 +939,7 @@ impl Connection { Ok(v::table .select((v::deployment, sql("created_at::text"))) .filter(v::id.eq(version)) - .first::<(String, String)>(&self.0) + .first::<(String, String)>(self.0.as_ref()) .optional()?) } @@ -980,7 +952,7 @@ impl Connection { Ok(s::table .select((s::current_version.nullable(), s::pending_version.nullable())) .filter(s::id.eq(subgraph_id)) - .first::<(Option, Option)>(&self.0) + .first::<(Option, Option)>(self.0.as_ref()) .optional()? .unwrap_or((None, None))) } @@ -1024,7 +996,7 @@ impl Connection { .on_conflict(u::id) .do_nothing() .returning(u::id) - .get_results::(&self.0)?) + .get_results::(self.0.as_ref())?) } /// Add details from the deployment shard to unuseddeployments @@ -1060,7 +1032,7 @@ impl Connection { u::failed.eq(detail.failed), u::synced.eq(detail.synced), )) - .execute(&self.0)?; + .execute(self.0.as_ref())?; } Ok(()) } @@ -1073,11 +1045,13 @@ impl Connection { use unused_deployments as u; match filter { - All => Ok(u::table.order_by(u::unused_at.desc()).load(&self.0)?), + All => Ok(u::table + .order_by(u::unused_at.desc()) + .load(self.0.as_ref())?), New => Ok(u::table .filter(u::removed_at.is_null()) .order_by(u::entity_count) - .load(&self.0)?), + .load(self.0.as_ref())?), } } @@ -1098,6 +1072,17 @@ impl Connection { .filter(v::deployment.eq(id.as_str())) .select(s::name) .distinct() - .load(&self.0)?) + .load(self.0.as_ref())?) + } + + pub fn find_ens_name(&self, hash: &str) -> Result, StoreError> { + use ens_names as dsl; + + dsl::table + .select(dsl::name) + .find(hash) + .get_result::(self.0.as_ref()) + .optional() + .map_err(|e| anyhow!("error looking up ens_name for hash {}: {}", hash, e).into()) } } diff --git a/store/postgres/src/query_store.rs b/store/postgres/src/query_store.rs index ffc914f26db..5f0f2686369 100644 --- a/store/postgres/src/query_store.rs +++ b/store/postgres/src/query_store.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use web3::types::H256; -use crate::store::ReplicaId; +use crate::deployment_store::{DeploymentStore, ReplicaId}; use graph::components::store::QueryStore as QueryStoreTrait; use graph::prelude::*; @@ -11,13 +11,13 @@ use crate::primary::Site; pub(crate) struct QueryStore { site: Arc, replica_id: ReplicaId, - store: Arc, + store: Arc, chain_store: Arc, } impl QueryStore { pub(crate) fn new( - store: Arc, + store: Arc, chain_store: Arc, site: Arc, replica_id: ReplicaId, @@ -67,20 +67,17 @@ impl QueryStoreTrait for QueryStore { // available. Ideally, we'd have the last REORG_THRESHOLD blocks in // memory so that we can check against them, and then mark in the // database the blocks on the main chain that we consider final - let subgraph_network = self.network_name()?; + let subgraph_network = self.network_name(); self.chain_store .block_number(block_hash)? .map(|(network_name, number)| { - if subgraph_network.is_none() || Some(&network_name) == subgraph_network.as_ref() { + if &network_name == subgraph_network { BlockNumber::try_from(number) .map_err(|e| StoreError::QueryExecutionError(e.to_string())) } else { Err(StoreError::QueryExecutionError(format!( "subgraph {} belongs to network {} but block {:x} belongs to network {}", - &self.site.deployment, - subgraph_network.unwrap_or("(none)".to_owned()), - block_hash, - network_name + &self.site.deployment, subgraph_network, block_hash, network_name ))) } }) @@ -103,10 +100,11 @@ impl QueryStoreTrait for QueryStore { .await } - fn deployment_state(&self) -> Result { + async fn deployment_state(&self) -> Result { Ok(self .store - .deployment_state_from_id(self.site.deployment.clone())?) + .deployment_state_from_id(self.site.deployment.clone()) + .await?) } fn api_schema(&self) -> Result, QueryExecutionError> { @@ -114,8 +112,7 @@ impl QueryStoreTrait for QueryStore { Ok(info.api) } - fn network_name(&self) -> Result, QueryExecutionError> { - let info = self.store.subgraph_info(&self.site.deployment)?; - Ok(info.network) + fn network_name(&self) -> &str { + &self.site.network } } diff --git a/store/postgres/src/relational_queries.rs b/store/postgres/src/relational_queries.rs index fabb66ac293..401152224ad 100644 --- a/store/postgres/src/relational_queries.rs +++ b/store/postgres/src/relational_queries.rs @@ -830,8 +830,14 @@ impl<'a> QueryFilter<'a> { } } Value::List(_) => { - out.push_identifier(column.name.as_str())?; - out.push_sql(" @> "); + if negated { + out.push_sql(" not "); + out.push_identifier(column.name.as_str())?; + out.push_sql(" && "); + } else { + out.push_identifier(column.name.as_str())?; + out.push_sql(" @> "); + } QueryValue(value, &column.column_type).walk_ast(out)?; } Value::Null diff --git a/store/postgres/src/sql_value.rs b/store/postgres/src/sql_value.rs index 4a4709144c9..e05fd5e6633 100644 --- a/store/postgres/src/sql_value.rs +++ b/store/postgres/src/sql_value.rs @@ -1,6 +1,7 @@ use diesel::pg::Pg; use diesel::serialize::{self, Output, ToSql}; -use diesel::sql_types::{Binary, Bool, Integer, Numeric, Text}; +use diesel::sql_types::{Binary, Bool, Integer, Text}; +use graph::prelude::anyhow::anyhow; use std::io::Write; use std::str::FromStr; @@ -17,53 +18,52 @@ impl SqlValue { impl ToSql for SqlValue { fn to_sql(&self, out: &mut Output) -> serialize::Result { - match self.0 { - Value::Bool(ref b) => >::to_sql(&b, out), - _ => panic!("Failed to convert non-boolean attribute value to boolean in SQL"), + match &self.0 { + Value::Bool(b) => >::to_sql(&b, out), + v => Err(anyhow!( + "Failed to convert non-boolean attribute value to boolean in SQL: {}", + v + ) + .into()), } } } impl ToSql for SqlValue { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - match self.0 { - Value::Int(ref i) => >::to_sql(&i, out), - _ => panic!("Failed to convert non-int attribute value to int in SQL"), - } - } -} - -// Used only for JSONB support -impl ToSql for SqlValue { fn to_sql(&self, out: &mut Output) -> serialize::Result { match &self.0 { - Value::BigDecimal(d) => <_ as ToSql>::to_sql(&d, out), - Value::BigInt(number) => { - <_ as ToSql>::to_sql(&scalar::BigDecimal::new(number.clone(), 0), out) - } - _ => panic!("Failed to convert attribute value to bigint in SQL"), + Value::Int(i) => >::to_sql(&i, out), + v => Err(anyhow!( + "Failed to convert non-int attribute value to int in SQL: {}", + v + ) + .into()), } } } impl ToSql for SqlValue { fn to_sql(&self, out: &mut Output) -> serialize::Result { - match self.0 { - Value::String(ref s) => >::to_sql(&s, out), - Value::Bytes(ref h) => >::to_sql(&h.to_string(), out), - _ => panic!("Failed to convert attribute value to String or Bytes in SQL"), + match &self.0 { + Value::String(s) => >::to_sql(&s, out), + Value::Bytes(h) => >::to_sql(&h.to_string(), out), + v => Err(anyhow!( + "Failed to convert attribute value to String or Bytes in SQL: {}", + v + ) + .into()), } } } impl ToSql for SqlValue { fn to_sql(&self, out: &mut Output) -> serialize::Result { - match self.0 { - Value::Bytes(ref h) => <_ as ToSql>::to_sql(&h.as_slice(), out), - Value::String(ref s) => { + match &self.0 { + Value::Bytes(h) => <_ as ToSql>::to_sql(&h.as_slice(), out), + Value::String(s) => { <_ as ToSql>::to_sql(scalar::Bytes::from_str(s)?.as_slice(), out) } - _ => panic!("Failed to convert attribute value to Bytes in SQL"), + v => Err(anyhow!("Failed to convert attribute value to Bytes in SQL: {}", v).into()), } } } diff --git a/store/postgres/src/store.rs b/store/postgres/src/store.rs index 4b9d484dff9..2705d9e143e 100644 --- a/store/postgres/src/store.rs +++ b/store/postgres/src/store.rs @@ -1,1336 +1,132 @@ -use detail::DeploymentDetail; -use diesel::connection::SimpleConnection; -use diesel::pg::PgConnection; -use diesel::prelude::*; -use diesel::r2d2::{ConnectionManager, PooledConnection}; -use diesel::{insert_into, update}; -use futures03::FutureExt as _; -use graph::components::store::{EntityType, StoredDynamicDataSource}; -use graph::data::subgraph::status; -use graph::prelude::{ - error, CancelGuard, CancelHandle, CancelToken, CancelableError, PoolWaitStats, - SubgraphDeploymentEntity, +use async_trait::async_trait; +use std::sync::Arc; + +use graph::{ + components::{ + server::index_node::VersionInfo, + store::{ + BlockStore as BlockStoreTrait, QueryStoreManager, StatusStore, + SubgraphStore as SubgraphStoreTrait, + }, + }, + constraint_violation, + data::subgraph::status, + prelude::{ + web3::types::Address, CheapClone, EthereumBlockPointer, QueryExecutionError, StoreError, + SubgraphDeploymentId, + }, }; -use lazy_static::lazy_static; -use lru_time_cache::LruCache; -use rand::{seq::SliceRandom, thread_rng}; -use std::convert::{TryFrom, TryInto}; -use std::iter::FromIterator; -use std::ops::Deref; -use std::sync::{atomic::AtomicUsize, Arc, Mutex}; -use std::time::Instant; -use std::{ - collections::{BTreeMap, HashMap}, - time::Duration, -}; -use tokio::sync::Semaphore; - -use graph::components::store::EntityCollection; -use graph::components::subgraph::ProofOfIndexingFinisher; -use graph::data::subgraph::schema::{SubgraphError, POI_OBJECT}; -use graph::prelude::{ - anyhow, debug, ethabi, futures03, info, o, tiny_keccak, tokio, trace, web3, ApiSchema, - BlockNumber, CheapClone, DeploymentState, DynTryFuture, Entity, EntityKey, EntityModification, - EntityOrder, EntityQuery, EntityRange, Error, EthereumBlockPointer, EthereumCallCache, Logger, - MetadataOperation, MetricsRegistry, QueryExecutionError, Schema, StopwatchMetrics, StoreError, - StoreEvent, SubgraphDeploymentId, Value, BLOCK_NUMBER_MAX, -}; - -use graph_graphql::prelude::api_schema; -use web3::types::{Address, H256}; - -use crate::primary::Site; -use crate::relational::{Layout, METADATA_LAYOUT}; -use crate::relational_queries::FromEntityData; -use crate::{connection_pool::ConnectionPool, detail, entities as e}; -use crate::{deployment, primary::Namespace}; -lazy_static! { - static ref CONNECTION_LIMITER: Semaphore = { - let db_conn_pool_size = std::env::var("STORE_CONNECTION_POOL_SIZE") - .unwrap_or("10".into()) - .parse::() - .expect("invalid STORE_CONNECTION_POOL_SIZE"); +use crate::{block_store::BlockStore, query_store::QueryStore, SubgraphStore}; - Semaphore::new(db_conn_pool_size) - }; -} - -embed_migrations!("./migrations"); - -/// Run all schema migrations. +/// The overall store of the system, consisting of a [SubgraphStore] and a +/// [BlockStore], each of which multiplex across multiple database shards. +/// The `SubgraphStore` is responsible for storing all data and metadata related +/// to individual subgraphs, and the `BlockStore` does the same for data belonging +/// to the chains that are being processed. /// -/// When multiple `graph-node` processes start up at the same time, we ensure -/// that they do not run migrations in parallel by using `blocking_conn` to -/// serialize them. The `conn` is used to run the actual migration. -fn initiate_schema(logger: &Logger, conn: &PgConnection, blocking_conn: &PgConnection) { - // Collect migration logging output - let mut output = vec![]; - - // Make sure the locking table exists so we have something - // to lock. We intentionally ignore errors here, because they are most - // likely caused by us losing a race to create the table against another - // graph-node. If this truly is an error, we will trip over it when - // we try to lock the table and report it to the user - if let Err(e) = blocking_conn.batch_execute( - "create table if not exists \ - __graph_node_global_lock(id int)", - ) { - debug!( - logger, - "Creating lock table failed, this is most likely harmless"; - "error" => format!("{:?}", e) - ); - } - - // blocking_conn holds the lock on the migrations table for the duration - // of the migration on conn. Since all nodes execute this code, only one - // of them can run this code at the same time. We need to use two - // connections for this because diesel will run each migration in its - // own txn, which makes it impossible to hold a lock across all of them - // on that connection - info!( - logger, - "Waiting for other graph-node instances to finish migrating" - ); - let result = blocking_conn.transaction(|| { - diesel::sql_query("lock table __graph_node_global_lock in exclusive mode") - .execute(blocking_conn)?; - info!(logger, "Running migrations"); - embedded_migrations::run_with_output(conn, &mut output) - }); - info!(logger, "Migrations finished"); - - // If there was any migration output, log it now - let has_output = !output.is_empty(); - if has_output { - let msg = String::from_utf8(output).unwrap_or_else(|_| String::from("")); - if result.is_err() { - error!(logger, "Postgres migration output"; "output" => msg); - } else { - debug!(logger, "Postgres migration output"; "output" => msg); - } - } - - if let Err(e) = result { - panic!( - "Error setting up Postgres database: \ - You may need to drop and recreate your database to work with the \ - latest version of graph-node. Error information: {:?}", - e - ) - }; - - if has_output { - // We take getting output as a signal that a migration was actually - // run, which is not easy to tell from the Diesel API, and reset the - // query statistics since a schema change makes them not all that - // useful. An error here is not serious and can be ignored. - conn.batch_execute("select pg_stat_statements_reset()").ok(); - } -} - -/// Configuration for the Diesel/Postgres store. -pub struct StoreConfig { - pub postgres_url: String, - pub network_name: String, -} - -/// When connected to read replicas, this allows choosing which DB server to use for an operation. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ReplicaId { - /// The main server has write and read access. - Main, - - /// A read replica identified by its index. - ReadOnly(usize), -} - -/// Commonly needed information about a subgraph that we cache in -/// `Store.subgraph_cache`. Only immutable subgraph data can be cached this -/// way as the cache lives for the lifetime of the `Store` object -#[derive(Clone)] -pub(crate) struct SubgraphInfo { - /// The schema as supplied by the user - pub(crate) input: Arc, - /// The schema we derive from `input` with `graphql::schema::api::api_schema` - pub(crate) api: Arc, - /// The name of the network from which the subgraph is syncing - pub(crate) network: Option, - /// The block number at which this subgraph was grafted onto - /// another one. We do not allow reverting past this block - pub(crate) graft_block: Option, - pub(crate) description: Option, - pub(crate) repository: Option, -} - -pub struct StoreInner { - logger: Logger, - - conn: ConnectionPool, - read_only_pools: Vec, - replica_order: Vec, - conn_round_robin_counter: AtomicUsize, - - /// A cache of commonly needed data about a subgraph. - subgraph_cache: Mutex>, - - /// A cache for the layout metadata for subgraphs. The Store just - /// hosts this because it lives long enough, but it is managed from - /// the entities module - pub(crate) layout_cache: e::LayoutCache, - - registry: Arc, -} - -/// A Store based on Diesel and Postgres. -#[derive(Clone)] -pub struct Store(Arc); - -impl CheapClone for Store {} - -impl Deref for Store { - type Target = StoreInner; - fn deref(&self) -> &Self::Target { - &self.0 - } +/// This struct should only be used during configuration and setup of `graph-node`. +/// Code that needs to access the store should use the traits from [graph::components::store] +/// and only require the smallest traits that are suitable for their purpose +pub struct Store { + subgraph_store: Arc, + block_store: Arc, } impl Store { - pub fn new( - logger: &Logger, - pool: ConnectionPool, - read_only_pools: Vec, - mut pool_weights: Vec, - registry: Arc, - ) -> Self { - // Create a store-specific logger - let logger = logger.new(o!("component" => "Store")); - - // Create the entities table (if necessary) - initiate_schema(&logger, &pool.get().unwrap(), &pool.get().unwrap()); - - // Create a list of replicas with repetitions according to the weights - // and shuffle the resulting list. Any missing weights in the list - // default to 1 - pool_weights.resize(read_only_pools.len() + 1, 1); - let mut replica_order: Vec<_> = pool_weights - .iter() - .enumerate() - .map(|(i, weight)| { - let replica = if i == 0 { - ReplicaId::Main - } else { - ReplicaId::ReadOnly(i - 1) - }; - vec![replica; *weight] - }) - .flatten() - .collect(); - let mut rng = thread_rng(); - replica_order.shuffle(&mut rng); - debug!(logger, "Using postgres host order {:?}", replica_order); - - // Create the store - let store = StoreInner { - logger: logger.clone(), - conn: pool, - read_only_pools, - replica_order, - conn_round_robin_counter: AtomicUsize::new(0), - subgraph_cache: Mutex::new(LruCache::with_capacity(100)), - layout_cache: e::make_layout_cache(), - registry, - }; - let store = Store(Arc::new(store)); - - // Return the store - store - } - - pub(crate) fn create_deployment( - &self, - schema: &Schema, - deployment: SubgraphDeploymentEntity, - site: &Site, - graft_site: Option, - replace: bool, - ) -> Result { - let conn = self.get_conn()?; - // This is a bit of a Frankenconnection: we don't have the actual - // layout yet; but for applying metadata, it's fine to use the metadata - // layout - let econn = e::Connection::new( - conn.into(), - METADATA_LAYOUT.clone(), - site.deployment.clone(), - ); - econn.transaction(|| -> Result<_, StoreError> { - let exists = deployment::exists(&econn.conn, &site.deployment)?; - - let event = if replace || !exists { - let ops = deployment.create_operations(&site.deployment); - self.apply_metadata_operations_with_conn(&econn, ops)? - } else { - StoreEvent::new(vec![]) - }; - - if !exists { - econn.create_schema(site.namespace.clone(), schema, graft_site)?; - } - Ok(event) - }) - } - - // Remove the data and metadata for the deployment `site`. This operation - // is not reversible - pub(crate) fn drop_deployment(&self, site: &Site) -> Result<(), StoreError> { - let conn = self.get_conn()?; - conn.transaction(|| e::Connection::drop_deployment(&conn, site)) - } - - /// Gets an entity from Postgres. - fn get_entity( - &self, - conn: &e::Connection, - key: &EntityKey, - ) -> Result, QueryExecutionError> { - // We should really have callers pass in a block number; but until - // that is fully plumbed in, we just use the biggest possible block - // number so that we will always return the latest version, - // i.e., the one with an infinite upper bound - conn.find(key, BLOCK_NUMBER_MAX).map_err(|e| { - QueryExecutionError::ResolveEntityError( - key.subgraph_id.clone(), - key.entity_type.to_string(), - key.entity_id.clone(), - format!("Invalid entity {}", e), - ) - }) - } - - pub(crate) fn execute_query( - &self, - conn: &e::Connection, - query: EntityQuery, - ) -> Result, QueryExecutionError> { - // Process results; deserialize JSON data - let logger = query.logger.unwrap_or(self.logger.clone()); - conn.query( - &logger, - query.collection, - query.filter, - query.order, - query.range, - query.block, - query.query_id, - ) - } - - fn check_interface_entity_uniqueness( - &self, - conn: &e::Connection, - key: &EntityKey, - ) -> Result<(), StoreError> { - // Collect all types that share an interface implementation with this - // entity type, and make sure there are no conflicting IDs. - // - // To understand why this is necessary, suppose that `Dog` and `Cat` are - // types and both implement an interface `Pet`, and both have instances - // with `id: "Fred"`. If a type `PetOwner` has a field `pets: [Pet]` - // then with the value `pets: ["Fred"]`, there's no way to disambiguate - // if that's Fred the Dog, Fred the Cat or both. - // - // This assumes that there are no concurrent writes to a subgraph. - let schema = self - .subgraph_info_with_conn(&conn.conn, &key.subgraph_id)? - .api; - let types_for_interface = schema.types_for_interface(); - let entity_type = match &key.entity_type { - EntityType::Data(s) => s, - EntityType::Metadata(_) => { - // Metadata has no interfaces - return Ok(()); - } - }; - let types_with_shared_interface = Vec::from_iter( - schema - .interfaces_for_type(entity_type) - .into_iter() - .flatten() - .map(|interface| &types_for_interface[&interface.name]) - .flatten() - .map(|object_type| &object_type.name) - .filter(|type_name| *type_name != entity_type), - ); - - if !types_with_shared_interface.is_empty() { - if let Some(conflicting_entity) = - conn.conflicting_entity(&key.entity_id, types_with_shared_interface)? - { - return Err(StoreError::ConflictingId( - entity_type.clone(), - key.entity_id.clone(), - conflicting_entity, - )); - } - } - Ok(()) - } - - /// Apply a metadata operation in Postgres. - fn apply_metadata_operation( - &self, - conn: &e::Connection, - operation: MetadataOperation, - ) -> Result { - match operation { - MetadataOperation::Set { key, data } => { - let key = key.into(); - - // Load the entity if exists - let entity = self.get_entity(conn, &key)?; - - // Identify whether this is an insert or an update operation and - // merge the changes into the entity. - let result = match entity { - Some(mut entity) => { - entity.merge_remove_null_fields(data); - conn.update(&key, entity, None).map(|_| 0) - } - None => { - // Merge with a new entity since that removes values that - // were set to Value::Null - let mut entity = Entity::new(); - entity.merge_remove_null_fields(data); - conn.insert(&key, entity, None).map(|_| 1) - } - }; - - result.map_err(|e| { - anyhow!( - "Failed to set entity ({}, {}, {}): {}", - key.subgraph_id, - key.entity_type, - key.entity_id, - e - ) - .into() - }) - } - MetadataOperation::Remove { .. } => unreachable!("metadata is never deleted"), + pub fn new(subgraph_store: Arc, block_store: Arc) -> Self { + Self { + subgraph_store, + block_store, } } - fn apply_entity_modifications( - &self, - conn: &e::Connection, - mods: Vec, - ptr: Option<&EthereumBlockPointer>, - stopwatch: StopwatchMetrics, - ) -> Result<(), StoreError> { - let mut count = 0; - - for modification in mods { - use EntityModification::*; - - let do_count = modification.entity_key().entity_type.is_data_type(); - let n = match modification { - Overwrite { key, data } => { - let section = stopwatch.start_section("check_interface_entity_uniqueness"); - self.check_interface_entity_uniqueness(conn, &key)?; - section.end(); - - let _section = stopwatch.start_section("apply_entity_modifications_update"); - conn.update(&key, data, ptr).map(|_| 0) - } - Insert { key, data } => { - let section = stopwatch.start_section("check_interface_entity_uniqueness"); - self.check_interface_entity_uniqueness(conn, &key)?; - section.end(); - - let _section = stopwatch.start_section("apply_entity_modifications_insert"); - conn.insert(&key, data, ptr).map(|_| 1) - } - Remove { key } => conn - .delete(&key, ptr) - // This conversion is ok since n will only be 0 or 1 - .map(|n| -(n as i32)) - .map_err(|e| { - anyhow!( - "Failed to remove entity ({}, {}, {}): {}", - key.subgraph_id, - key.entity_type, - key.entity_id, - e - ) - .into() - }), - }?; - if do_count { - count += n; - } - } - conn.update_entity_count(count)?; - Ok(()) + pub fn subgraph_store(&self) -> Arc { + self.subgraph_store.cheap_clone() } - pub(crate) fn apply_metadata_operations_with_conn( - &self, - econn: &e::Connection, - operations: Vec, - ) -> Result { - // Emit a store event for the changes we are about to make - let event: StoreEvent = operations.clone().into(); - - // Actually apply the operations - for operation in operations.into_iter() { - self.apply_metadata_operation(econn, operation)?; - } - Ok(event) + pub fn block_store(&self) -> Arc { + self.block_store.cheap_clone() } +} - /// Execute a closure with a connection to the database. - /// - /// # API - /// The API of using a closure to bound the usage of the connection serves several - /// purposes: - /// - /// * Moves blocking database access out of the `Future::poll`. Within - /// `Future::poll` (which includes all `async` methods) it is illegal to - /// perform a blocking operation. This includes all accesses to the - /// database, acquiring of locks, etc. Calling a blocking operation can - /// cause problems with `Future` combinators (including but not limited - /// to select, timeout, and FuturesUnordered) and problems with - /// executors/runtimes. This method moves the database work onto another - /// thread in a way which does not block `Future::poll`. - /// - /// * Limit the total number of connections. Because the supplied closure - /// takes a reference, we know the scope of the usage of all entity - /// connections and can limit their use in a non-blocking way. - /// - /// # Cancellation - /// The normal pattern for futures in Rust is drop to cancel. Once we - /// spawn the database work in a thread though, this expectation no longer - /// holds because the spawned task is the independent of this future. So, - /// this method provides a cancel token which indicates that the `Future` - /// has been dropped. This isn't *quite* as good as drop on cancel, - /// because a drop on cancel can do things like cancel http requests that - /// are in flight, but checking for cancel periodically is a significant - /// improvement. - /// - /// The implementation of the supplied closure should check for cancel - /// between every operation that is potentially blocking. This includes - /// any method which may interact with the database. The check can be - /// conveniently written as `token.check_cancel()?;`. It is low overhead - /// to check for cancel, so when in doubt it is better to have too many - /// checks than too few. - /// - /// # Panics: - /// * This task will panic if the supplied closure panics - /// * This task will panic if the supplied closure returns Err(Cancelled) - /// when the supplied cancel token is not cancelled. - pub(crate) async fn with_conn( +#[async_trait] +impl QueryStoreManager for Store { + async fn query_store( &self, - f: impl 'static - + Send - + FnOnce( - &PooledConnection>, - &CancelHandle, - ) -> Result>, - ) -> Result { - let _permit = CONNECTION_LIMITER.acquire().await; - let store = self.clone(); - - let cancel_guard = CancelGuard::new(); - let cancel_handle = cancel_guard.handle(); - - let result = graph::spawn_blocking_allow_panic(move || { - // It is possible time has passed between scheduling on the - // threadpool and being executed. Time to check for cancel. - cancel_handle.check_cancel()?; - - // A failure to establish a connection is propagated as though the - // closure failed. - let conn = store - .get_conn() - .map_err(|e| CancelableError::Error(StoreError::Unknown(e)))?; - - // It is possible time has passed while establishing a connection. - // Time to check for cancel. - cancel_handle.check_cancel()?; - - f(&conn, &cancel_handle) + target: graph::data::query::QueryTarget, + for_subscription: bool, + ) -> Result< + Arc, + graph::prelude::QueryExecutionError, + > { + let store = self.subgraph_store.cheap_clone(); + let (store, site, replica) = graph::spawn_blocking_allow_panic(move || { + store + .replica_for_query(target, for_subscription) + .map_err(|e| e.into()) }) .await - .unwrap(); // Propagate panics, though there shouldn't be any. - - drop(cancel_guard); + .map_err(|e| QueryExecutionError::Panic(e.to_string())) + .and_then(|x| x)?; + + let chain_store = self.block_store.chain_store(&site.network).ok_or_else(|| { + constraint_violation!( + "Subgraphs index a known network, but {} indexes `{}` which we do not know about. This is most likely a configuration error.", + site.deployment, + site.network + ) + })?; - // Finding cancel isn't technically unreachable, since there is nothing - // stopping the supplied closure from returning Canceled even if the - // supplied handle wasn't canceled. That would be very unexpected, the - // doc comment for this function says we will panic in this scenario. - match result { - Ok(t) => Ok(t), - Err(CancelableError::Error(e)) => Err(e), - Err(CancelableError::Cancel) => panic!("The closure supplied to with_entity_conn must not return Err(Canceled) unless the supplied token was canceled."), - } + Ok(Arc::new(QueryStore::new(store, chain_store, site, replica))) } +} - /// Executes a closure with an `e::Connection` reference. - /// The `e::Connection` gives access to subgraph specific storage in the database - mostly for - /// storing and loading entities local to that subgraph. - /// - /// This uses `with_conn` under the hood. Please see it's documentation for important details - /// about usage. - async fn with_entity_conn( - self: Arc, - site: Arc, - f: impl 'static - + Send - + FnOnce(&e::Connection, &CancelHandle) -> Result>, - ) -> Result { - let store = self.cheap_clone(); - - // Duplicated logic: No need to make re-usable when the - // other end will go away. - // See also 220c1ae9-3e8a-42d3-bcc5-b1244a69b8a9 - let start = Instant::now(); - let registry = self.registry.cheap_clone(); - - self.with_conn(move |conn, cancel_handle| { - registry - .global_deployment_counter( - "deployment_get_entity_conn_secs", - "total time spent getting an entity connection", - site.deployment.as_str(), - ) - .map_err(|e| CancelableError::Error(StoreError::Unknown(e.into())))? - .inc_by(start.elapsed().as_secs_f64()); - - cancel_handle.check_cancel()?; - let layout = store.layout(&conn, &site.namespace, &site.deployment)?; - cancel_handle.check_cancel()?; - let conn = e::Connection::new(conn.into(), layout, site.deployment.clone()); +impl StatusStore for Store { + fn status(&self, filter: status::Filter) -> Result, StoreError> { + let mut infos = self.subgraph_store.status(filter)?; + let ptrs = self.block_store.chain_head_pointers()?; - f(&conn, cancel_handle) - }) - .await - } - - /// Deprecated. Use `with_conn` instead. - pub(crate) fn get_conn( - &self, - ) -> Result>, Error> { - loop { - match self.conn.get_timeout(Duration::from_secs(60)) { - Ok(conn) => return Ok(conn), - Err(e) => error!(self.logger, "Error checking out connection, retrying"; - "error" => e.to_string(), - ), + for info in &mut infos { + for chain in &mut info.chains { + chain.chain_head_block = ptrs.get(&chain.network).map(|ptr| ptr.to_owned().into()); } } + Ok(infos) } - /// Panics if `idx` is not a valid index for a read only pool. - fn read_only_conn( - &self, - idx: usize, - ) -> Result>, Error> { - self.read_only_pools[idx].get().map_err(Error::from) - } - - // Duplicated logic - this function may eventually go away. - // See also 220c1ae9-3e8a-42d3-bcc5-b1244a69b8a9 - /// Deprecated. Use `with_entity_conn` instead - pub(crate) fn get_entity_conn( - &self, - site: &Site, - replica: ReplicaId, - ) -> Result { - assert!(!site.namespace.is_metadata()); - - let start = Instant::now(); - let conn = match replica { - ReplicaId::Main => self.get_conn()?, - ReplicaId::ReadOnly(idx) => self.read_only_conn(idx)?, - }; - self.registry - .global_deployment_counter( - "deployment_get_entity_conn_secs", - "total time spent getting an entity connection", - site.deployment.as_str(), - )? - .inc_by(start.elapsed().as_secs_f64()); - let data = self.layout(&conn, &site.namespace, &site.deployment)?; - Ok(e::Connection::new( - conn.into(), - data, - site.deployment.clone(), - )) - } - - pub(crate) fn wait_stats(&self, replica: ReplicaId) -> &PoolWaitStats { - match replica { - ReplicaId::Main => &self.conn.wait_stats, - ReplicaId::ReadOnly(idx) => &self.read_only_pools[idx].wait_stats, - } - } - - /// Return the layout for the subgraph. Since constructing a `Layout` - /// object takes a bit of computation, we cache layout objects that do - /// not have a pending migration in the Store, i.e., for the lifetime of - /// the Store. Layout objects with a pending migration can not be - /// cached for longer than a transaction since they might change - /// without us knowing - pub(crate) fn layout( - &self, - conn: &PgConnection, - namespace: &Namespace, - subgraph: &SubgraphDeploymentId, - ) -> Result, StoreError> { - assert!(!namespace.is_metadata()); - - if let Some(layout) = self.layout_cache.lock().unwrap().get(subgraph) { - return Ok(layout.clone()); - } - - let layout = Arc::new(e::Connection::layout(conn, namespace.clone(), subgraph)?); - if layout.is_cacheable() { - &self - .layout_cache - .lock() - .unwrap() - .insert(subgraph.clone(), layout.clone()); - } - Ok(layout.clone()) - } - - fn subgraph_info_with_conn( - &self, - conn: &PgConnection, - subgraph_id: &SubgraphDeploymentId, - ) -> Result { - if let Some(info) = self.subgraph_cache.lock().unwrap().get(&subgraph_id) { - return Ok(info.clone()); - } - - let (input_schema, description, repository) = - deployment::manifest_info(&conn, subgraph_id.to_owned())?; - let network = deployment::network(&conn, &subgraph_id)?; - - let graft_block = - deployment::graft_point(&conn, &subgraph_id)?.map(|(_, ptr)| ptr.number as i32); - - let features = deployment::features(&conn, subgraph_id)?; - - // Generate an API schema for the subgraph and make sure all types in the - // API schema have a @subgraphId directive as well - let mut schema = input_schema.clone(); - schema.document = - api_schema(&schema.document, &features).map_err(|e| StoreError::Unknown(e.into()))?; - schema.add_subgraph_id_directives(subgraph_id.clone()); - - let info = SubgraphInfo { - input: Arc::new(input_schema), - api: Arc::new(ApiSchema::from_api_schema(schema)?), - network, - graft_block, - description, - repository, - }; - - // Insert the schema into the cache. - let mut cache = self.subgraph_cache.lock().unwrap(); - cache.insert(subgraph_id.clone(), info); - - Ok(cache.get(&subgraph_id).unwrap().clone()) - } - - pub(crate) fn subgraph_info( - &self, - subgraph_id: &SubgraphDeploymentId, - ) -> Result { - if let Some(info) = self.subgraph_cache.lock().unwrap().get(&subgraph_id) { - return Ok(info.clone()); - } + fn version_info(&self, version_id: &str) -> Result { + let mut info = self.subgraph_store.version_info(version_id)?; - let conn = self.get_conn()?; - self.subgraph_info_with_conn(&conn, subgraph_id) - } + info.total_ethereum_blocks_count = self.block_store.chain_head_block(&info.network)?; - fn block_ptr_with_conn( - subgraph_id: &SubgraphDeploymentId, - conn: &e::Connection, - ) -> Result, Error> { - Ok(deployment::block_ptr(&conn.conn, subgraph_id)?) + Ok(info) } - pub(crate) fn deployment_details( + fn versions_for_subgraph_id( &self, - ids: Vec, - ) -> Result, StoreError> { - let conn = self.get_conn()?; - conn.transaction(|| -> Result<_, StoreError> { detail::deployment_details(&conn, ids) }) + subgraph_id: &str, + ) -> Result<(Option, Option), StoreError> { + self.subgraph_store.versions_for_subgraph_id(subgraph_id) } - pub(crate) fn deployment_statuses( - &self, - ids: Vec, - ) -> Result, StoreError> { - let conn = self.get_conn()?; - conn.transaction(|| -> Result, StoreError> { - detail::deployment_statuses(&conn, ids) - }) - } -} - -/// Methods that back the trait `graph::components::Store`, but have small -/// variations in their signatures -impl Store { - pub(crate) fn block_ptr(&self, site: &Site) -> Result, Error> { - Self::block_ptr_with_conn( - &site.deployment, - &self - .get_entity_conn(site, ReplicaId::Main) - .map_err(|e| QueryExecutionError::StoreError(e.into()))?, - ) - } - - pub(crate) fn supports_proof_of_indexing<'a>( + fn supports_proof_of_indexing<'a>( self: Arc, - site: Arc, - ) -> DynTryFuture<'a, bool> { - async move { - self.with_entity_conn(site, |conn, cancel| { - cancel.check_cancel()?; - Ok(conn.supports_proof_of_indexing()) - }) - .await - .map_err(|e| e.into()) - } - .boxed() + subgraph_id: &'a SubgraphDeploymentId, + ) -> graph::prelude::DynTryFuture<'a, bool> { + self.subgraph_store + .clone() + .supports_proof_of_indexing(subgraph_id) } - pub(crate) fn get_proof_of_indexing<'a>( + fn get_proof_of_indexing<'a>( self: Arc, - site: Arc, + subgraph_id: &'a SubgraphDeploymentId, indexer: &'a Option
, block: EthereumBlockPointer, - ) -> DynTryFuture<'a, Option<[u8; 32]>> { - let logger = self.logger.cheap_clone(); - let indexer = indexer.clone(); - let site2 = site.clone(); - let site3 = site.clone(); - - async move { - let entities = self - .with_entity_conn(site2, move |conn, cancel| { - cancel.check_cancel()?; - - if !conn.supports_proof_of_indexing() { - return Ok(None); - } - - conn.transaction::<_, CancelableError, _>(move || { - let latest_block_ptr = - match Self::block_ptr_with_conn(&site.deployment, conn)? { - Some(inner) => inner, - None => return Ok(None), - }; - - cancel.check_cancel()?; - - // FIXME: (Determinism) - // - // It is vital to ensure that the block hash given in the query - // is a parent of the latest block indexed for the subgraph. - // Unfortunately the machinery needed to do this is not yet in place. - // The best we can do right now is just to make sure that the block number - // is high enough. - if latest_block_ptr.number < block.number { - return Ok(None); - } - - let entities = conn - .query::( - &logger, - EntityCollection::All(vec![POI_OBJECT.to_owned()]), - None, - EntityOrder::Default, - EntityRange { - first: None, - skip: 0, - }, - block.number.try_into().unwrap(), - None, - ) - .map_err(anyhow::Error::from)?; - - Ok(Some(entities)) - }) - .map_err(|e| e.into()) - }) - .await?; - - let entities = if let Some(entities) = entities { - entities - } else { - return Ok(None); - }; - - let mut by_causality_region = entities - .into_iter() - .map(|e| { - let causality_region = e.id()?; - let digest = match e.get("digest") { - Some(Value::Bytes(b)) => Ok(b.to_owned()), - other => Err(anyhow::anyhow!( - "Entity has non-bytes digest attribute: {:?}", - other - )), - }?; - - Ok((causality_region, digest)) - }) - .collect::, anyhow::Error>>()?; - - let mut finisher = ProofOfIndexingFinisher::new(&block, &site3.deployment, &indexer); - for (name, region) in by_causality_region.drain() { - finisher.add_causality_region(&name, ®ion); - } - - Ok(Some(finisher.finish())) - } - .boxed() - } - - pub(crate) fn get( - &self, - site: &Site, - key: EntityKey, - ) -> Result, QueryExecutionError> { - let conn = self - .get_entity_conn(site, ReplicaId::Main) - .map_err(|e| QueryExecutionError::StoreError(e.into()))?; - self.get_entity(&conn, &key) - } - - pub(crate) fn get_many( - &self, - site: &Site, - ids_for_type: BTreeMap<&EntityType, Vec<&str>>, - ) -> Result>, StoreError> { - if ids_for_type.is_empty() { - return Ok(BTreeMap::new()); - } - let conn = self - .get_entity_conn(site, ReplicaId::Main) - .map_err(|e| QueryExecutionError::StoreError(e.into()))?; - conn.find_many(ids_for_type, BLOCK_NUMBER_MAX) - } - - pub(crate) fn find( - &self, - site: &Site, - query: EntityQuery, - ) -> Result, QueryExecutionError> { - let conn = self - .get_entity_conn(site, ReplicaId::Main) - .map_err(|e| QueryExecutionError::StoreError(e.into()))?; - self.execute_query(&conn, query) - } - - pub(crate) fn find_one( - &self, - site: &Site, - mut query: EntityQuery, - ) -> Result, QueryExecutionError> { - query.range = EntityRange::first(1); - - let conn = self - .get_entity_conn(site, ReplicaId::Main) - .map_err(|e| QueryExecutionError::StoreError(e.into()))?; - - let mut results = self.execute_query(&conn, query)?; - match results.len() { - 0 | 1 => Ok(results.pop()), - n => panic!("find_one query found {} results", n), - } - } - - pub(crate) fn find_ens_name(&self, hash: &str) -> Result, QueryExecutionError> { - use crate::db_schema::ens_names as dsl; - - let conn = self - .get_conn() - .map_err(|e| QueryExecutionError::StoreError(e.into()))?; - - dsl::table - .select(dsl::name) - .find(hash) - .get_result::(&conn) - .optional() - .map_err(|e| { - QueryExecutionError::StoreError( - anyhow!("error looking up ens_name for hash {}: {}", hash, e).into(), - ) - }) - } - - pub(crate) fn transact_block_operations( - &self, - site: &Site, - block_ptr_to: EthereumBlockPointer, - mods: Vec, - stopwatch: StopwatchMetrics, - deterministic_errors: Vec, - ) -> Result { - // All operations should apply only to data or metadata for this subgraph - if mods - .iter() - .map(|modification| modification.entity_key()) - .any(|key| key.subgraph_id != site.deployment) - { - panic!( - "transact_block_operations must affect only entities \ - in the subgraph or in the subgraph of subgraphs" - ); - } - - let econn = self.get_entity_conn(site, ReplicaId::Main)?; - - let event = econn.transaction(|| -> Result<_, StoreError> { - let block_ptr_from = Self::block_ptr_with_conn(&site.deployment, &econn)?; - if let Some(ref block_ptr_from) = block_ptr_from { - if block_ptr_from.number >= block_ptr_to.number { - return Err(StoreError::DuplicateBlockProcessing( - site.deployment.clone(), - block_ptr_to.number, - )); - } - } - - // Emit a store event for the changes we are about to make. We - // wait with sending it until we have done all our other work - // so that we do not hold a lock on the notification queue - // for longer than we have to - let event: StoreEvent = mods.iter().collect(); - - // Make the changes - let section = stopwatch.start_section("apply_entity_modifications"); - self.apply_entity_modifications(&econn, mods, Some(&block_ptr_to), stopwatch)?; - section.end(); - - if !deterministic_errors.is_empty() { - deployment::insert_subgraph_errors( - &econn.conn, - &site.deployment, - deterministic_errors, - )?; - } - - let metadata_event = - deployment::forward_block_ptr(&econn.conn, &site.deployment, block_ptr_to)?; - Ok(event.extend(metadata_event)) - })?; - - Ok(event) - } - - pub(crate) fn revert_block_operations( - &self, - site: &Site, - block_ptr_to: EthereumBlockPointer, - ) -> Result { - let econn = self.get_entity_conn(site, ReplicaId::Main)?; - - let event = econn.transaction(|| -> Result<_, StoreError> { - // Unwrap: If we are reverting then the block ptr is not `None`. - let block_ptr_from = Self::block_ptr_with_conn(&site.deployment, &econn)?.unwrap(); - - // Sanity check on block numbers - if block_ptr_from.number != block_ptr_to.number + 1 { - panic!("revert_block_operations must revert a single block only"); - } - - // Don't revert past a graft point - let info = self.subgraph_info_with_conn(&econn.conn, &site.deployment)?; - if let Some(graft_block) = info.graft_block { - if graft_block as u64 > block_ptr_to.number { - return Err(anyhow!( - "Can not revert subgraph `{}` to block {} as it was \ - grafted at block {} and reverting past a graft point \ - is not possible", - site.deployment.clone(), - block_ptr_to.number, - graft_block - ) - .into()); - } - } - - let metadata_event = - deployment::revert_block_ptr(&econn.conn, &site.deployment, block_ptr_to)?; - - let (event, count) = econn.revert_block(&block_ptr_from)?; - econn.update_entity_count(count)?; - Ok(event.extend(metadata_event)) - })?; - - Ok(event) - } - - pub(crate) fn deployment_state_from_id( - &self, - id: SubgraphDeploymentId, - ) -> Result { - let conn = self.get_conn()?; - deployment::state(&conn, id) - } - - pub(crate) async fn fail_subgraph( - &self, - id: SubgraphDeploymentId, - error: SubgraphError, - ) -> Result<(), StoreError> { - self.with_conn(move |conn, _| { - conn.transaction(|| deployment::fail(&conn, &id, error)) - .map_err(|e| e.into()) - }) - .await?; - Ok(()) - } - - #[allow(dead_code)] - pub(crate) fn block_number( - &self, - subgraph_id: &SubgraphDeploymentId, - hash: H256, - ) -> Result, StoreError> { - use crate::db_schema::ethereum_blocks::dsl; - - // We should also really check that the block with the given hash is - // on the chain starting at the subgraph's current head. That check is - // very expensive though with the data structures we have currently - // available. Ideally, we'd have the last REORG_THRESHOLD blocks in - // memory so that we can check against them, and then mark in the - // database the blocks on the main chain that we consider final - let block: Option<(i64, String)> = dsl::ethereum_blocks - .select((dsl::number, dsl::network_name)) - .filter(dsl::hash.eq(format!("{:x}", hash))) - .first(&*self.get_conn()?) - .optional()?; - let subgraph_network = self.subgraph_info(subgraph_id)?.network; - block - .map(|(number, network_name)| { - if subgraph_network.is_none() || Some(&network_name) == subgraph_network.as_ref() { - BlockNumber::try_from(number) - .map_err(|e| StoreError::QueryExecutionError(e.to_string())) - } else { - Err(StoreError::QueryExecutionError(format!( - "subgraph {} belongs to network {} but block {:x} belongs to network {}", - subgraph_id, - subgraph_network.unwrap_or("(none)".to_owned()), - hash, - network_name - ))) - } - }) - .transpose() - } - - pub(crate) fn replica_for_query( - &self, - for_subscription: bool, - ) -> Result { - use std::sync::atomic::Ordering; - - let replica_id = match for_subscription { - // Pick a weighted ReplicaId. `replica_order` contains a list of - // replicas with repetitions according to their weight - false => { - let weights_count = self.replica_order.len(); - let index = - self.conn_round_robin_counter.fetch_add(1, Ordering::SeqCst) % weights_count; - *self.replica_order.get(index).unwrap() - } - // Subscriptions always go to the main replica. - true => ReplicaId::Main, - }; - - Ok(replica_id) - } - - pub(crate) async fn load_dynamic_data_sources( - &self, - id: SubgraphDeploymentId, - ) -> Result, StoreError> { - self.with_conn(move |conn, _| { - conn.transaction(|| crate::dynds::load(&conn, id.as_str())) - .map_err(|e| e.into()) - }) - .await - } - - pub(crate) fn exists_and_synced(&self, id: &SubgraphDeploymentId) -> Result { - let conn = self.get_conn()?; - conn.transaction(|| deployment::exists_and_synced(&conn, id)) - } - - pub(crate) fn graft_pending( - &self, - id: &SubgraphDeploymentId, - ) -> Result, StoreError> { - let conn = self.get_conn()?; - deployment::graft_pending(&conn, id) - } - - pub(crate) fn start_subgraph( - &self, - logger: &Logger, - site: Arc, - graft_base: Option<(Site, EthereumBlockPointer)>, - ) -> Result<(), StoreError> { - let econn = self.get_entity_conn(&site, ReplicaId::Main)?; - econn.transaction(|| { - deployment::unfail(&econn.conn, &site.deployment)?; - econn.start_subgraph(logger, graft_base) - }) - } - - #[cfg(debug_assertions)] - pub fn error_count(&self, id: &SubgraphDeploymentId) -> Result { - let conn = self.get_conn()?; - deployment::error_count(&conn, id) - } -} - -impl EthereumCallCache for Store { - fn get_call( - &self, - contract_address: ethabi::Address, - encoded_call: &[u8], - block: EthereumBlockPointer, - ) -> Result>, Error> { - use crate::db_schema::{eth_call_cache, eth_call_meta}; - use diesel::dsl::sql; - - let id = contract_call_id(&contract_address, encoded_call, &block); - let conn = &*self.get_conn()?; - if let Some(call_output) = conn.transaction::<_, Error, _>(|| { - if let Some((return_value, update_accessed_at)) = eth_call_cache::table - .find(id.as_ref()) - .inner_join(eth_call_meta::table) - .select(( - eth_call_cache::return_value, - sql("CURRENT_DATE > eth_call_meta.accessed_at"), - )) - .get_result(conn) - .optional()? - { - if update_accessed_at { - update(eth_call_meta::table.find(contract_address.as_ref())) - .set(eth_call_meta::accessed_at.eq(sql("CURRENT_DATE"))) - .execute(conn)?; - } - Ok(Some(return_value)) - } else { - Ok(None) - } - })? { - Ok(Some(call_output)) - } else { - // No entry with the new id format, try the old one. - let old_id = old_contract_call_id(&contract_address, &encoded_call, &block); - if let Some(return_value) = eth_call_cache::table - .find(old_id.as_ref()) - .select(eth_call_cache::return_value) - .get_result::>(conn) - .optional()? - { - use crate::db_schema::eth_call_cache::dsl; - - // Once we stop getting these logs, this code can be removed. - trace!(self.logger, "Updating eth call cache entry"); - - // Migrate to the new format by re-inserting the call and deleting the old entry. - self.set_call(contract_address, encoded_call, block, &return_value)?; - diesel::delete(eth_call_cache::table.filter(dsl::id.eq(old_id.as_ref()))) - .execute(conn)?; - Ok(Some(return_value)) - } else { - Ok(None) - } - } - } - - fn set_call( - &self, - contract_address: ethabi::Address, - encoded_call: &[u8], - block: EthereumBlockPointer, - return_value: &[u8], - ) -> Result<(), Error> { - use crate::db_schema::{eth_call_cache, eth_call_meta}; - use diesel::dsl::sql; - - let id = contract_call_id(&contract_address, encoded_call, &block); - let conn = &*self.get_conn()?; - conn.transaction(|| { - insert_into(eth_call_cache::table) - .values(( - eth_call_cache::id.eq(id.as_ref()), - eth_call_cache::contract_address.eq(contract_address.as_ref()), - eth_call_cache::block_number.eq(block.number as i32), - eth_call_cache::return_value.eq(return_value), - )) - .on_conflict_do_nothing() - .execute(conn)?; - - let accessed_at = eth_call_meta::accessed_at.eq(sql("CURRENT_DATE")); - insert_into(eth_call_meta::table) - .values(( - eth_call_meta::contract_address.eq(contract_address.as_ref()), - accessed_at.clone(), - )) - .on_conflict(eth_call_meta::contract_address) - .do_update() - .set(accessed_at) - .execute(conn) - .map(|_| ()) - .map_err(Error::from) - }) + ) -> graph::prelude::DynTryFuture<'a, Option<[u8; 32]>> { + self.subgraph_store + .clone() + .get_proof_of_indexing(subgraph_id, indexer, block) } } - -/// Deprecated format for the contract call id. -fn old_contract_call_id( - contract_address: ðabi::Address, - encoded_call: &[u8], - block: &EthereumBlockPointer, -) -> [u8; 16] { - let mut id = [0; 16]; - let mut hash = tiny_keccak::Keccak::new_shake128(); - hash.update(contract_address.as_ref()); - hash.update(encoded_call); - hash.update(block.hash.as_ref()); - hash.finalize(&mut id); - id -} - -/// The id is the hashed encoded_call + contract_address + block hash to uniquely identify the call. -/// 256 bits of output, and therefore 128 bits of security against collisions, are needed since this -/// could be targeted by a birthday attack. -fn contract_call_id( - contract_address: ðabi::Address, - encoded_call: &[u8], - block: &EthereumBlockPointer, -) -> [u8; 32] { - let mut hash = blake3::Hasher::new(); - hash.update(encoded_call); - hash.update(contract_address.as_ref()); - hash.update(block.hash.as_ref()); - *hash.finalize().as_bytes() -} diff --git a/store/postgres/src/sharded_store.rs b/store/postgres/src/subgraph_store.rs similarity index 75% rename from store/postgres/src/sharded_store.rs rename to store/postgres/src/subgraph_store.rs index 1129a4ce439..be7f85e0814 100644 --- a/store/postgres/src/sharded_store.rs +++ b/store/postgres/src/subgraph_store.rs @@ -1,7 +1,13 @@ -use diesel::Connection; -use std::fmt; +use diesel::{ + pg::Pg, + serialize::Output, + sql_types::Text, + types::{FromSql, ToSql}, +}; +use std::iter::FromIterator; use std::sync::RwLock; use std::{collections::BTreeMap, collections::HashMap, sync::Arc}; +use std::{fmt, io::Write}; use graph::{ components::{ @@ -16,24 +22,24 @@ use graph::{ prelude::StoreEvent, prelude::SubgraphDeploymentEntity, prelude::{ - lazy_static, web3::types::Address, ApiSchema, DeploymentState, DynTryFuture, Entity, - EntityKey, EntityModification, EntityQuery, Error, EthereumBlockPointer, EthereumCallCache, - Logger, MetadataOperation, NodeId, QueryExecutionError, Schema, StopwatchMetrics, - Store as StoreTrait, StoreError, SubgraphDeploymentId, SubgraphName, + lazy_static, o, web3::types::Address, ApiSchema, CheapClone, DeploymentState, DynTryFuture, + Entity, EntityKey, EntityModification, EntityQuery, Error, EthereumBlockPointer, Logger, + MetadataOperation, MetricsRegistry, NodeId, QueryExecutionError, Schema, StopwatchMetrics, + StoreError, SubgraphDeploymentId, SubgraphName, SubgraphStore as SubgraphStoreTrait, SubgraphVersionSwitchingMode, }, }; use store::StoredDynamicDataSource; -use crate::{deployment, primary, primary::Site}; +use crate::{connection_pool::ConnectionPool, primary, primary::Site}; use crate::{ + deployment_store::{DeploymentStore, ReplicaId}, detail::DeploymentDetail, primary::UnusedDeployment, - store::{ReplicaId, Store}, }; /// The name of a database shard; valid names must match `[a-z0-9_]+` -#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, AsExpression, FromSqlRow)] pub struct Shard(String); lazy_static! { @@ -77,6 +83,19 @@ impl fmt::Display for Shard { } } +impl FromSql for Shard { + fn from_sql(bytes: Option<&[u8]>) -> diesel::deserialize::Result { + let s = >::from_sql(bytes)?; + Shard::new(s).map_err(Into::into) + } +} + +impl ToSql for Shard { + fn to_sql(&self, out: &mut Output) -> diesel::serialize::Result { + >::to_sql(&self.0, out) + } +} + /// Decide where a new deployment should be placed based on the subgraph name /// and the network it is indexing. If the deployment can be placed, returns /// the name of the database shard for the deployment and the names of the @@ -96,28 +115,111 @@ pub mod unused { } } -/// Multiplex store operations on subgraphs and deployments between a primary -/// and any number of additional storage shards. See [this document](../../docs/sharded.md) -/// for details on how storage is split up -pub struct ShardedStore { - primary: Arc, - stores: HashMap>, +/// Multiplex store operations on subgraphs and deployments between a +/// primary and any number of additional storage shards. The primary +/// contains information about named subgraphs, and how the underlying +/// deployments are spread across shards, while the actual deployment data +/// and metadata is stored in the shards. Depending on the configuration, +/// the database for the primary and for the shards can be the same +/// database, in which case they are all backed by one connection pool, or +/// separate databases in the same Postgres cluster, or entirely separate +/// clusters. Details of how to configure shards can be found in [this +/// document](https://github.com/graphprotocol/graph-node/blob/master/docs/sharding.md) +/// +/// The primary uses the following database tables: +/// - `public.deployment_schemas`: immutable data about deployments, including +/// the shard that stores the deployment data and metadata, the namespace in +/// the shard that contains the deployment data, and the network/chain that +/// the deployment is indexing +/// - `subgraphs.subgraph` and `subgraphs.subgraph_version`: information about +/// named subgraphs and how they map to deployments +/// - `subgraphs.subgraph_deployment_assignment`: which index node is indexing +/// what deployment +/// +/// For each deployment, the corresponding shard contains a namespace for +/// the deployment data; the schema in that namespace is generated from the +/// deployment's GraphQL schema by the [crate::relational::Layout], which +/// is also responsible for modifying and querying subgraph +/// data. Deployment metadata is stored in tables in the `subgraphs` +/// namespace in the same shard as the deployment data. The most important +/// of these tables are +/// +/// - `subgraphs.subgraph_deployment`: the main table for deployment metadata; +/// most importantly, it stores the pointer to the current subgraph head, i.e., +/// the block up to which the subgraph has indexed the chain, together with +/// other things like whether the subgraph has synced, whether it has failed +/// and whether it encountered any errors +/// - `subgraphs.subgraph_manifest`: immutable information derived from the YAML +/// manifest for the deployment +/// - `subgraphs.dynamic_ethereum_contract_data_source`: the data sources that +/// the subgraph has created from templates in the manifest. Some detail about +/// dynamic data sources is also stored in `subgraphs.ethereum_contract_source` +/// - `subgraphs.subgraph_error`: details about errors that the deployment has +/// encountered +/// +/// There are more metadata tables, but they are rarely if ever read, only +/// written to when a deployment is created and should be removed in a +/// future version of `graph-node` +/// +/// The `SubgraphStore` mostly orchestrates access to the primary and the +/// shards. The actual work is done by code in the `primary` module for +/// queries against the primary store, and by the `DeploymentStore` for +/// access to deployment data and metadata. +pub struct SubgraphStore { + logger: Logger, + primary: ConnectionPool, + stores: HashMap>, /// Cache for the mapping from deployment id to shard/namespace/id sites: RwLock>>, placer: Arc, } -impl ShardedStore { +impl SubgraphStore { + /// Create a new store for subgraphs that distributes deployments across + /// multiple databases + /// + /// `stores` is a list of the shards. The tuple contains the shard name, the main + /// connection pool for the database, a list of read-only connections + /// for the same database, and a list of weights determining how often + /// to use the main pool and the read replicas for queries. The list + /// of weights must be one longer than the list of read replicas, and + /// `weights[0]` is used for the main pool. + /// + /// All write operations for a shard are performed against the main + /// pool. One of the shards must be named `primary` + /// + /// The `placer` determines where `create_subgraph_deployment` puts a new deployment pub fn new( - stores: HashMap>, + logger: &Logger, + stores: Vec<(Shard, ConnectionPool, Vec, Vec)>, placer: Arc, + registry: Arc, ) -> Self { let primary = stores - .get(&PRIMARY_SHARD) - .expect("we always have a primary store") - .clone(); + .iter() + .find(|(name, _, _, _)| name == &*PRIMARY_SHARD) + .map(|(_, pool, _, _)| pool.clone()) + .expect("we always have a primary shard"); + let stores = HashMap::from_iter(stores.into_iter().map( + |(name, main_pool, read_only_pools, weights)| { + let logger = logger.new(o!("shard" => name.to_string())); + + ( + name, + Arc::new(DeploymentStore::new( + &logger, + main_pool, + read_only_pools, + weights, + registry.cheap_clone(), + )), + ) + }, + )); let sites = RwLock::new(HashMap::new()); + let logger = logger.new(o!("shard" => PRIMARY_SHARD.to_string())); Self { + logger, primary, stores, sites, @@ -160,7 +262,10 @@ impl ShardedStore { Ok(()) } - fn store(&self, id: &SubgraphDeploymentId) -> Result<(&Arc, Arc), StoreError> { + fn store( + &self, + id: &SubgraphDeploymentId, + ) -> Result<(&Arc, Arc), StoreError> { let site = self.site(id)?; let store = self .stores @@ -224,7 +329,7 @@ impl ShardedStore { // TODO: Check this for behavior on failure let site = self .primary_conn()? - .allocate_site(shard.clone(), &schema.id, &network_name)?; + .allocate_site(shard.clone(), &schema.id, network_name)?; let graft_site = deployment .graft_base @@ -248,8 +353,7 @@ impl ShardedStore { let exists_and_synced = |id: &SubgraphDeploymentId| { let (store, _) = self.store(id)?; - let conn = store.get_conn()?; - deployment::exists_and_synced(&conn, id.as_str()) + store.deployment_exists_and_synced(id) }; // FIXME: This simultaneously holds a `primary_conn` and a shard connection, which can @@ -290,16 +394,31 @@ impl ShardedStore { conn.send_store_event(event) } + /// Get a connection to the primary shard. Code must never hold one of these + /// connections while also accessing a `DeploymentStore`, since both + /// might draw connections from the same pool, and trying to get two + /// connections can deadlock the entire process if the pool runs out + /// of connections in between getting the first one and trying to get the + /// second one. fn primary_conn(&self) -> Result { - let conn = self.primary.get_conn()?; + let conn = self.primary.get_with_timeout_warning(&self.logger)?; Ok(primary::Connection::new(conn)) } + pub(crate) async fn with_primary_conn( + &self, + f: impl 'static + Send + FnOnce(primary::Connection) -> Result, + ) -> Result { + self.primary + .with_conn(|conn, _| f(primary::Connection::new(conn)).map_err(|e| e.into())) + .await + } + pub(crate) fn replica_for_query( &self, target: QueryTarget, for_subscription: bool, - ) -> Result<(Arc, Arc, ReplicaId), StoreError> { + ) -> Result<(Arc, Arc, ReplicaId), StoreError> { let id = match target { QueryTarget::Name(name) => { let conn = self.primary_conn()?; @@ -319,42 +438,17 @@ impl ShardedStore { /// it very hard to export items just for testing #[cfg(debug_assertions)] pub fn delete_all_entities_for_test_use_only(&self) -> Result<(), StoreError> { - use diesel::connection::SimpleConnection; - let pconn = self.primary_conn()?; let schemas = pconn.sites()?; // Delete all subgraph schemas for schema in schemas { let (store, _) = self.store(&schema.deployment)?; - let conn = store.get_conn()?; - deployment::drop_schema(&conn, &schema.namespace)?; + store.drop_deployment_schema(&schema.namespace)?; } - // Delete metadata entities in each shard - // Generated by running 'layout -g delete subgraphs.graphql' - let query = " - delete from subgraphs.ethereum_block_handler_filter_entity; - delete from subgraphs.ethereum_contract_source; - delete from subgraphs.dynamic_ethereum_contract_data_source; - delete from subgraphs.ethereum_contract_abi; - delete from subgraphs.subgraph; - delete from subgraphs.subgraph_deployment; - delete from subgraphs.ethereum_block_handler_entity; - delete from subgraphs.subgraph_deployment_assignment; - delete from subgraphs.ethereum_contract_mapping; - delete from subgraphs.subgraph_version; - delete from subgraphs.subgraph_manifest; - delete from subgraphs.ethereum_call_handler_entity; - delete from subgraphs.ethereum_contract_data_source; - delete from subgraphs.ethereum_contract_data_source_template; - delete from subgraphs.ethereum_contract_data_source_template_source; - delete from subgraphs.ethereum_contract_event_handler; - "; for store in self.stores.values() { - let conn = store.get_conn()?; - conn.batch_execute(query)?; - conn.batch_execute("delete from deployment_schemas;")?; + store.drop_all_metadata()?; } self.clear_caches(); Ok(()) @@ -472,6 +566,87 @@ impl ShardedStore { Ok(()) } + pub(crate) fn status(&self, filter: status::Filter) -> Result, StoreError> { + let deployments = match filter { + status::Filter::SubgraphName(name) => { + let deployments = self.primary_conn()?.deployments_for_subgraph(name)?; + if deployments.is_empty() { + return Ok(Vec::new()); + } + deployments + } + status::Filter::SubgraphVersion(name, use_current) => { + let deployment = self.primary_conn()?.subgraph_version(name, use_current)?; + match deployment { + Some(deployment) => vec![deployment], + None => { + return Ok(Vec::new()); + } + } + } + status::Filter::Deployments(deployments) => deployments, + }; + + let by_shard: HashMap>> = self.deployments_by_shard(deployments)?; + + // Go shard-by-shard to look up deployment statuses + let mut infos = Vec::new(); + for (shard, sites) in by_shard.into_iter() { + let store = self + .stores + .get(&shard) + .ok_or(StoreError::UnknownShard(shard.to_string()))?; + infos.extend(store.deployment_statuses(&sites)?); + } + let infos = self.primary_conn()?.fill_assignments(infos)?; + Ok(infos) + } + + pub(crate) fn version_info(&self, version: &str) -> Result { + if let Some((deployment_id, created_at)) = self.primary_conn()?.version_info(version)? { + let id = SubgraphDeploymentId::new(deployment_id.clone()) + .map_err(|id| constraint_violation!("illegal deployment id {}", id))?; + let (store, site) = self.store(&id)?; + let statuses = store.deployment_statuses(&vec![site])?; + let status = statuses + .first() + .ok_or_else(|| StoreError::DeploymentNotFound(deployment_id.clone()))?; + let chain = status + .chains + .first() + .ok_or_else(|| constraint_violation!("no chain info for {}", deployment_id))?; + let latest_ethereum_block_number = + chain.latest_block.as_ref().map(|ref block| block.number()); + let subgraph_info = store.subgraph_info(&id)?; + let network = self.network_name(&id)?; + + let info = VersionInfo { + created_at, + deployment_id, + latest_ethereum_block_number, + total_ethereum_blocks_count: None, + synced: status.synced, + failed: status.health.is_failed(), + description: subgraph_info.description, + repository: subgraph_info.repository, + schema: subgraph_info.input, + network: network.to_string(), + }; + Ok(info) + } else { + Err(StoreError::DeploymentNotFound(version.to_string())) + } + } + + pub(crate) fn versions_for_subgraph_id( + &self, + subgraph_id: &str, + ) -> Result<(Option, Option), StoreError> { + let primary = self.primary_conn()?; + + primary.versions_for_subgraph_id(subgraph_id) + } + #[cfg(debug_assertions)] pub fn error_count(&self, id: &SubgraphDeploymentId) -> Result { let (store, _) = self.store(id)?; @@ -480,7 +655,7 @@ impl ShardedStore { } #[async_trait::async_trait] -impl StoreTrait for ShardedStore { +impl SubgraphStoreTrait for SubgraphStore { fn block_ptr(&self, id: &SubgraphDeploymentId) -> Result, Error> { let (store, site) = self.store(id)?; store.block_ptr(site.as_ref()) @@ -529,7 +704,7 @@ impl StoreTrait for ShardedStore { } fn find_ens_name(&self, hash: &str) -> Result, QueryExecutionError> { - self.primary.find_ens_name(hash) + Ok(self.primary_conn()?.find_ens_name(hash)?) } fn transact_block_operations( @@ -565,20 +740,22 @@ impl StoreTrait for ShardedStore { self.send_store_event(&event) } - fn deployment_state_from_name( + async fn deployment_state_from_name( &self, name: SubgraphName, ) -> Result { - let id = self.primary_conn()?.current_deployment_for_subgraph(name)?; - self.deployment_state_from_id(id) + let id = self + .with_primary_conn(|conn| conn.current_deployment_for_subgraph(name)) + .await?; + self.deployment_state_from_id(id).await } - fn deployment_state_from_id( + async fn deployment_state_from_id( &self, id: SubgraphDeploymentId, ) -> Result { let (store, _) = self.store(&id)?; - store.deployment_state_from_id(id) + store.deployment_state_from_id(id).await } fn start_subgraph_deployment( @@ -598,6 +775,11 @@ impl StoreTrait for ShardedStore { store.start_subgraph(logger, site, graft_base) } + fn unfail(&self, id: &SubgraphDeploymentId) -> Result<(), StoreError> { + let (store, site) = self.store(id)?; + store.unfail(site) + } + fn is_deployment_synced(&self, id: &SubgraphDeploymentId) -> Result { let (store, _) = self.store(&id)?; Ok(store.exists_and_synced(&id)?) @@ -605,6 +787,9 @@ impl StoreTrait for ShardedStore { fn deployment_synced(&self, id: &SubgraphDeploymentId) -> Result<(), Error> { let event = { + // Make sure we drop `pconn` before we call into the deployment + // store so that we do not hold two database connections which + // might come from the same pool and could therefore deadlock let pconn = self.primary_conn()?; pconn.transaction(|| -> Result<_, Error> { let changes = pconn.promote_deployment(id)?; @@ -613,11 +798,8 @@ impl StoreTrait for ShardedStore { }; let (dstore, _) = self.store(id)?; - { - // Do not hold dconn and primary_conn() at the same time. - let dconn = dstore.get_conn()?; - dconn.transaction(|| deployment::set_synced(&dconn, id))?; - } + dstore.deployment_synced(id)?; + Ok(self.primary_conn()?.send_store_event(&event)?) } @@ -675,47 +857,6 @@ impl StoreTrait for ShardedStore { }) } - fn status(&self, filter: status::Filter) -> Result, StoreError> { - let deployments = match filter { - status::Filter::SubgraphName(name) => { - let deployments = self.primary_conn()?.deployments_for_subgraph(name)?; - if deployments.is_empty() { - return Ok(Vec::new()); - } - deployments - } - status::Filter::SubgraphVersion(name, use_current) => { - let deployment = self.primary_conn()?.subgraph_version(name, use_current)?; - match deployment { - Some(deployment) => vec![deployment], - None => { - return Ok(Vec::new()); - } - } - } - status::Filter::Deployments(deployments) => deployments, - }; - - let by_shard: HashMap>> = self.deployments_by_shard(deployments)?; - - // Go shard-by-shard to look up deployment statuses - let mut infos = Vec::new(); - for (shard, ids) in by_shard.into_iter() { - let store = self - .stores - .get(&shard) - .ok_or(StoreError::UnknownShard(shard.to_string()))?; - let ids = ids - .into_iter() - .map(|site| site.deployment.to_string()) - .collect(); - infos.extend(store.deployment_statuses(ids)?); - } - let infos = self.primary_conn()?.fill_assignments(infos)?; - let infos = self.primary_conn()?.fill_chain_head_pointers(infos)?; - Ok(infos) - } - async fn load_dynamic_data_sources( &self, id: SubgraphDeploymentId, @@ -760,82 +901,9 @@ impl StoreTrait for ShardedStore { Ok(info.api) } - fn network_name(&self, id: &SubgraphDeploymentId) -> Result, StoreError> { - let (store, _) = self.store(&id)?; - let info = store.subgraph_info(id)?; - Ok(info.network) - } - - fn version_info(&self, version: &str) -> Result { - if let Some((deployment_id, created_at)) = self.primary_conn()?.version_info(version)? { - let id = SubgraphDeploymentId::new(deployment_id.clone()) - .map_err(|id| constraint_violation!("illegal deployment id {}", id))?; - let (store, _) = self.store(&id)?; - let statuses = store.deployment_statuses(vec![deployment_id.clone()])?; - let status = statuses - .first() - .ok_or_else(|| StoreError::DeploymentNotFound(deployment_id.clone()))?; - let chain = status - .chains - .first() - .ok_or_else(|| constraint_violation!("no chain info for {}", deployment_id))?; - let latest_ethereum_block_number = - chain.latest_block.as_ref().map(|ref block| block.number()); - let subgraph_info = store.subgraph_info(&id)?; - let total_ethereum_blocks_count = subgraph_info - .network - .as_ref() - .map(|network| self.primary_conn()?.chain_head_block(network)) - .transpose()? - .flatten(); - - let info = VersionInfo { - created_at, - deployment_id, - latest_ethereum_block_number, - total_ethereum_blocks_count, - synced: status.synced, - failed: status.health.is_failed(), - description: subgraph_info.description, - repository: subgraph_info.repository, - schema: subgraph_info.input, - network: subgraph_info.network, - }; - Ok(info) - } else { - Err(StoreError::DeploymentNotFound(version.to_string())) - } - } - - fn versions_for_subgraph_id( - &self, - subgraph_id: &str, - ) -> Result<(Option, Option), StoreError> { - let primary = self.primary_conn()?; - - primary.versions_for_subgraph_id(subgraph_id) - } -} - -impl EthereumCallCache for ShardedStore { - fn get_call( - &self, - contract_address: Address, - encoded_call: &[u8], - block: EthereumBlockPointer, - ) -> Result>, Error> { - self.primary.get_call(contract_address, encoded_call, block) - } - - fn set_call( - &self, - contract_address: Address, - encoded_call: &[u8], - block: EthereumBlockPointer, - return_value: &[u8], - ) -> Result<(), Error> { - self.primary - .set_call(contract_address, encoded_call, block, return_value) + fn network_name(&self, id: &SubgraphDeploymentId) -> Result { + let (_, site) = self.store(&id)?; + Ok(site.network.to_string()) } } diff --git a/store/postgres/src/subgraphs.graphql b/store/postgres/src/subgraphs.graphql index 992c60c0d26..b3b9587d1ff 100644 --- a/store/postgres/src/subgraphs.graphql +++ b/store/postgres/src/subgraphs.graphql @@ -17,7 +17,7 @@ type SubgraphVersion @entity { # We store the id of a SubgraphDeployment, but since when we read # through GraphQL, we pull back a SubgraphDeploymentDetail which # contains a superset of the attributes of a SubgraphDeployment - deployment: SubgraphDeploymentDetail! + deployment: SubgraphDeployment! createdAt: BigInt! } @@ -56,40 +56,6 @@ type SubgraphDeployment @entity { maxReorgDepth: Int } -# This is not a real entity type. It is a view that can be queried, but -# not modified -type SubgraphDeploymentDetail @entity { - id: ID! # Subgraph IPFS hash - manifest: SubgraphManifest! - failed: Boolean! @deprecated(reason: "Use `health`.") - health: Health! - synced: Boolean! - - "If the subgraph has failed, this is the error caused it" - fatalError: SubgraphError - - "Sorted by block number, limited to 1000" - nonFatalErrors: [SubgraphError!]! - - earliestEthereumBlockHash: Bytes - earliestEthereumBlockNumber: BigInt - latestEthereumBlockHash: Bytes - latestEthereumBlockNumber: BigInt - entityCount: BigInt! - dynamicDataSources: [DynamicEthereumContractDataSource!] @derivedFrom(field: "deployment") - graftBase: SubgraphDeployment - graftBlockHash: Bytes - graftBlockNumber: BigInt - # From ethereum_networks - ethereumHeadBlockNumber: BigInt - ethereumHeadBlockHash: Bytes - # From the first EthereumContractDataSource - network: String - # From SubgraphDeploymentAssignment, it is nullable here - # as not every deployment is assigned - nodeId: String -} - type SubgraphDeploymentAssignment @entity { id: ID! # Subgraph IPFS hash nodeId: String! diff --git a/store/postgres/tests/chain_head.rs b/store/postgres/tests/chain_head.rs index 774a174550a..c84b33a5d08 100644 --- a/store/postgres/tests/chain_head.rs +++ b/store/postgres/tests/chain_head.rs @@ -1,13 +1,16 @@ //! Test ChainStore implementation of Store, in particular, how //! the chain head pointer gets updated in various situations -use futures::future::IntoFuture; -use std::fmt::Debug; +use std::future::Future; use std::sync::Arc; -use graph::prelude::{Future01CompatExt, SubgraphDeploymentId}; -use graph::{components::store::ChainStore, prelude::QueryStoreManager}; -use graph_store_postgres::NetworkStore as DieselStore; +use graph::prelude::QueryStoreManager; +use graph::prelude::{anyhow::anyhow, anyhow::Error}; +use graph::{cheap_clone::CheapClone, prelude::web3::types::H160}; +use graph::{components::store::BlockStore as _, prelude::SubgraphDeploymentId}; +use graph::{components::store::ChainStore as _, prelude::EthereumCallCache as _}; +use graph_store_postgres::Store as DieselStore; +use graph_store_postgres::{layout_for_tests::FAKE_NETWORK_SHARED, ChainStore as DieselChainStore}; use test_store::block_store::{ Chain, FakeBlock, BLOCK_FIVE, BLOCK_FOUR, BLOCK_ONE, BLOCK_ONE_NO_PARENT, BLOCK_ONE_SIBLING, @@ -20,28 +23,42 @@ use test_store::*; const ANCESTOR_COUNT: u64 = 3; /// Test harness for running database integration tests. -fn run_test(chain: Chain, test: F) +fn run_test(chain: Chain, test: F) where - F: FnOnce(Arc) -> R + Send + 'static, - R: IntoFuture + Send + 'static, - R::Error: Send + Debug, - R::Future: Send, + F: Fn(Arc, Arc) -> Result<(), Error> + Send + 'static, { run_test_sequentially( || (), |store, ()| async move { - // Reset state before starting - block_store::remove(); + for name in vec![NETWORK_NAME, FAKE_NETWORK_SHARED] { + block_store::set_chain(chain.clone(), name); - // Seed database with test data - block_store::insert(chain, NETWORK_NAME); + let chain_store = store.block_store().chain_store(name).expect("chain store"); - // Run test - test(store) - .into_future() - .compat() - .await - .expect("test finishes successfully"); + // Run test + test(chain_store.cheap_clone(), store.cheap_clone()) + .expect(&format!("test finishes successfully on network {}", name)); + } + }, + ); +} + +fn run_test_async(chain: Chain, test: F) +where + F: Fn(Arc, Arc) -> R + Send + Sync + 'static, + R: Future + Send + 'static, +{ + run_test_sequentially( + || (), + |store, ()| async move { + for name in vec![NETWORK_NAME, FAKE_NETWORK_SHARED] { + block_store::set_chain(chain.clone(), name); + + let chain_store = store.block_store().chain_store(name).expect("chain store"); + + // Run test + test(chain_store.cheap_clone(), store.clone()).await; + } }, ); } @@ -56,7 +73,7 @@ fn check_chain_head_update( head_exp: Option<&'static FakeBlock>, missing: Option<&'static str>, ) { - run_test(chain, move |store| -> Result<(), ()> { + run_test(chain, move |store, _| { let missing_act: Vec<_> = store .attempt_chain_head_update(ANCESTOR_COUNT) .expect("attempt_chain_head_update failed") @@ -154,26 +171,31 @@ fn block_number() { let chain = vec![&*GENESIS_BLOCK, &*BLOCK_ONE, &*BLOCK_TWO]; let subgraph = SubgraphDeploymentId::new("nonExistentSubgraph").unwrap(); - run_test(chain, move |store| -> Result<(), ()> { - create_test_subgraph(&subgraph, "type Dummy @entity { id: ID! }"); - - let query_store = store.query_store(subgraph.into(), false).unwrap(); - let block = query_store - .block_number(GENESIS_BLOCK.block_hash()) - .expect("Found genesis block"); - assert_eq!(Some(0), block); - - let block = query_store - .block_number(BLOCK_ONE.block_hash()) - .expect("Found block 1"); - assert_eq!(Some(1), block); - - let block = query_store - .block_number(BLOCK_THREE.block_hash()) - .expect("Looked for block 3"); - assert!(block.is_none()); + run_test_async(chain, move |_, subgraph_store| { + let subgraph = subgraph.cheap_clone(); + async move { + create_test_subgraph(&subgraph, "type Dummy @entity { id: ID! }"); - Ok(()) + let query_store = subgraph_store + .query_store(subgraph.cheap_clone().into(), false) + .await + .unwrap(); + + let block = query_store + .block_number(GENESIS_BLOCK.block_hash()) + .expect("Found genesis block"); + assert_eq!(Some(0), block); + + let block = query_store + .block_number(BLOCK_ONE.block_hash()) + .expect("Found block 1"); + assert_eq!(Some(1), block); + + let block = query_store + .block_number(BLOCK_THREE.block_hash()) + .expect("Looked for block 3"); + assert!(block.is_none()); + } }) } @@ -185,7 +207,7 @@ fn block_hashes_by_number() { &*BLOCK_TWO, &*BLOCK_TWO_NO_PARENT, ]; - run_test(chain, move |store| -> Result<(), ()> { + run_test(chain, move |store, _| { let hashes = store.block_hashes_by_block_number(1).unwrap(); assert_eq!(vec![BLOCK_ONE.block_hash()], hashes); @@ -221,3 +243,119 @@ fn block_hashes_by_number() { Ok(()) }) } + +#[track_caller] +fn check_ancestor( + store: &Arc, + child: &FakeBlock, + offset: u64, + exp: &FakeBlock, +) -> Result<(), Error> { + let act = store + .ancestor_block(child.block_ptr(), offset)? + .ok_or_else(|| anyhow!("block {} has no ancestor at offset {}", child.hash, offset))?; + let act_hash = format!("{:x}", act.block.hash.unwrap()); + let exp_hash = &exp.hash; + + if &act_hash != exp_hash { + Err(anyhow!( + "expected hash `{}` but got `{}`", + exp_hash, + act_hash + )) + } else { + Ok(()) + } +} + +#[test] +fn ancestor_block_simple() { + let chain = vec![ + &*GENESIS_BLOCK, + &*BLOCK_ONE, + &*BLOCK_TWO, + &*BLOCK_THREE, + &*BLOCK_FOUR, + &*BLOCK_FIVE, + ]; + + run_test(chain, move |store, _| -> Result<(), Error> { + check_ancestor(&store, &*BLOCK_FIVE, 1, &*BLOCK_FOUR)?; + check_ancestor(&store, &*BLOCK_FIVE, 2, &*BLOCK_THREE)?; + check_ancestor(&store, &*BLOCK_FIVE, 3, &*BLOCK_TWO)?; + check_ancestor(&store, &*BLOCK_FIVE, 4, &*BLOCK_ONE)?; + check_ancestor(&store, &*BLOCK_FIVE, 5, &*GENESIS_BLOCK)?; + check_ancestor(&store, &*BLOCK_THREE, 2, &*BLOCK_ONE)?; + + for offset in [6, 7, 8, 50].iter() { + let offset = offset.clone() as u64; + let res = store.ancestor_block(BLOCK_FIVE.block_ptr(), offset); + assert!(res.is_err()); + } + + let block = store.ancestor_block(BLOCK_TWO_NO_PARENT.block_ptr(), 1)?; + assert!(block.is_none()); + Ok(()) + }); +} + +#[test] +fn ancestor_block_ommers() { + let chain = vec![ + &*GENESIS_BLOCK, + &*BLOCK_ONE, + &*BLOCK_ONE_SIBLING, + &*BLOCK_TWO, + ]; + + run_test(chain, move |store, _| -> Result<(), Error> { + check_ancestor(&store, &*BLOCK_ONE, 1, &*GENESIS_BLOCK)?; + check_ancestor(&store, &*BLOCK_ONE_SIBLING, 1, &*GENESIS_BLOCK)?; + check_ancestor(&store, &*BLOCK_TWO, 1, &*BLOCK_ONE)?; + check_ancestor(&store, &*BLOCK_TWO, 2, &*GENESIS_BLOCK)?; + Ok(()) + }); +} + +#[test] +fn eth_call_cache() { + let chain = vec![&*GENESIS_BLOCK, &*BLOCK_ONE, &*BLOCK_TWO]; + + run_test(chain, |store, _| { + let address = H160([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + let call: [u8; 6] = [1, 2, 3, 4, 5, 6]; + let return_value: [u8; 3] = [7, 8, 9]; + + store + .set_call(address, &call, BLOCK_ONE.block_ptr(), &return_value) + .unwrap(); + + let ret = store + .get_call(address, &call, GENESIS_BLOCK.block_ptr()) + .unwrap(); + assert!(ret.is_none()); + + let ret = store + .get_call(address, &call, BLOCK_ONE.block_ptr()) + .unwrap() + .unwrap(); + assert_eq!(&return_value, ret.as_slice()); + + let ret = store + .get_call(address, &call, BLOCK_TWO.block_ptr()) + .unwrap(); + assert!(ret.is_none()); + + let new_return_value: [u8; 3] = [10, 11, 12]; + store + .set_call(address, &call, BLOCK_TWO.block_ptr(), &new_return_value) + .unwrap(); + let ret = store + .get_call(address, &call, BLOCK_TWO.block_ptr()) + .unwrap() + .unwrap(); + assert_eq!(&new_return_value, ret.as_slice()); + + Ok(()) + }) +} diff --git a/store/postgres/tests/graft.rs b/store/postgres/tests/graft.rs index fef67013fd2..cc954479a5a 100644 --- a/store/postgres/tests/graft.rs +++ b/store/postgres/tests/graft.rs @@ -8,7 +8,7 @@ use graph::data::store::scalar; use graph::data::subgraph::schema::*; use graph::data::subgraph::*; use graph::prelude::*; -use graph_store_postgres::NetworkStore as DieselStore; +use graph_store_postgres::SubgraphStore as DieselSubgraphStore; use web3::types::H256; const USER_GQL: &str = " @@ -84,12 +84,12 @@ lazy_static! { /// Test harness for running database integration tests. fn run_test(test: F) where - F: FnOnce(Arc) -> R + Send + 'static, + F: FnOnce(Arc) -> R + Send + 'static, R: IntoFuture + Send + 'static, R::Error: Send + Debug, R::Future: Send, { - let store = STORE.clone(); + let store = STORE.subgraph_store(); // Lock regardless of poisoning. This also forces sequential test execution. let mut runtime = match STORE_RUNTIME.lock() { @@ -115,7 +115,7 @@ where /// /// Inserts data in test blocks 1, 2, and 3, leaving test blocks 3A, 4, and 4A for the tests to /// use. -fn insert_test_data(store: Arc) { +fn insert_test_data(store: Arc) { let manifest = SubgraphManifest { id: TEST_SUBGRAPH_ID.clone(), location: "/ipfs/test".to_owned(), @@ -252,7 +252,7 @@ fn create_test_entity( } /// Removes test data from the database behind the store. -fn remove_test_data(store: Arc) { +fn remove_test_data(store: Arc) { store .delete_all_entities_for_test_use_only() .expect("deleting test entities succeeds"); @@ -262,6 +262,7 @@ fn remove_test_data(store: Arc) { fn graft() { run_test(move |store| -> Result<(), ()> { const SUBGRAPH: &str = "grafted"; + let subgraph_id = SubgraphDeploymentId::new(SUBGRAPH).unwrap(); let res = test_store::create_grafted_subgraph( &subgraph_id, diff --git a/store/postgres/tests/relational.rs b/store/postgres/tests/relational.rs index aa188f09be3..e30f20756b4 100644 --- a/store/postgres/tests/relational.rs +++ b/store/postgres/tests/relational.rs @@ -745,6 +745,25 @@ fn check_find() { .check(vec![], drinks_query(vec!["beer", "water"])) .check(vec![], drinks_query(vec!["beer", "wine", "water"])); + // list not contains + let checker = checker + // User 3 do not have "beer" on its drinks list. + .check( + vec!["3"], + user_query().filter(EntityFilter::NotContains( + "drinks".into(), + vec!["beer"].into(), + )), + ) + // Users 2 do not have "tea" on its drinks list. + .check( + vec!["2"], + user_query().filter(EntityFilter::NotContains( + "drinks".into(), + vec!["tea"].into(), + )), + ); + // string attributes let checker = checker .check( diff --git a/store/postgres/tests/store.rs b/store/postgres/tests/store.rs index b148f999770..8fe31aba60c 100644 --- a/store/postgres/tests/store.rs +++ b/store/postgres/tests/store.rs @@ -7,14 +7,15 @@ use std::{collections::HashSet, sync::Mutex}; use test_store::*; use graph::components::store::{ - EntityFilter, EntityKey, EntityOrder, EntityQuery, EntityType, SubscriptionManager as _, + BlockStore as _, EntityFilter, EntityKey, EntityOrder, EntityQuery, EntityType, StatusStore, + SubscriptionManager as _, }; use graph::data::store::scalar; use graph::data::subgraph::schema::*; use graph::data::subgraph::*; use graph::prelude::*; use graph_store_postgres::layout_for_tests::STRING_PREFIX_SIZE; -use graph_store_postgres::NetworkStore as DieselStore; +use graph_store_postgres::{Store as DieselStore, SubgraphStore as DieselSubgraphStore}; use web3::types::{Address, H256}; const USER_GQL: &str = " @@ -124,11 +125,12 @@ where run_test_sequentially( || (), |store, ()| async move { + let subgraph_store = store.subgraph_store(); // Reset state before starting - remove_test_data(store.clone()); + remove_test_data(subgraph_store.clone()); // Seed database with test data - insert_test_data(store.clone()); + insert_test_data(subgraph_store.clone()); // Run test test(store).await @@ -140,7 +142,7 @@ where /// /// Inserts data in test blocks `GENESIS_PTR`, `TEST_BLOCK_1_PTR`, and /// `TEST_BLOCK_2_PTR` -fn insert_test_data(store: Arc) { +fn insert_test_data(store: Arc) { let manifest = SubgraphManifest { id: TEST_SUBGRAPH_ID.clone(), location: "/ipfs/test".to_owned(), @@ -277,7 +279,7 @@ fn create_test_entity( } /// Removes test data from the database behind the store. -fn remove_test_data(store: Arc) { +fn remove_test_data(store: Arc) { store .delete_all_entities_for_test_use_only() .expect("deleting test entities succeeds"); @@ -297,11 +299,15 @@ fn delete_entity() { let entity_key = EntityKey::data(TEST_SUBGRAPH_ID.clone(), USER.to_owned(), "3".to_owned()); // Check that there is an entity to remove. - store.get(entity_key.clone()).unwrap().unwrap(); + store + .subgraph_store() + .get(entity_key.clone()) + .unwrap() + .unwrap(); let count = get_entity_count(store.clone(), &TEST_SUBGRAPH_ID); transact_entity_operations( - &store, + &store.subgraph_store(), TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_3_PTR, vec![EntityOperation::Remove { @@ -315,7 +321,7 @@ fn delete_entity() { ); // Check that that the deleted entity id is not present - assert!(store.get(entity_key).unwrap().is_none()); + assert!(store.subgraph_store().get(entity_key).unwrap().is_none()); }) } @@ -324,7 +330,7 @@ fn delete_entity() { fn get_entity_1() { run_test(|store| async move { let key = EntityKey::data(TEST_SUBGRAPH_ID.clone(), USER.to_owned(), "1".to_owned()); - let result = store.get(key).unwrap(); + let result = store.subgraph_store().get(key).unwrap(); let mut expected_entity = Entity::new(); @@ -355,7 +361,7 @@ fn get_entity_1() { fn get_entity_3() { run_test(|store| async move { let key = EntityKey::data(TEST_SUBGRAPH_ID.clone(), USER.to_owned(), "3".to_owned()); - let result = store.get(key).unwrap(); + let result = store.subgraph_store().get(key).unwrap(); let mut expected_entity = Entity::new(); @@ -397,7 +403,7 @@ fn insert_entity() { ); let count = get_entity_count(store.clone(), &TEST_SUBGRAPH_ID); transact_entity_operations( - &store, + &store.subgraph_store(), TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_3_PTR, vec![test_entity], @@ -409,7 +415,7 @@ fn insert_entity() { ); // Check that new record is in the store - store.get(entity_key).unwrap().unwrap(); + store.subgraph_store().get(entity_key).unwrap().unwrap(); }) } @@ -434,12 +440,19 @@ fn update_existing() { }; // Verify that the entity before updating is different from what we expect afterwards - assert_ne!(store.get(entity_key.clone()).unwrap().unwrap(), new_data); + assert_ne!( + store + .subgraph_store() + .get(entity_key.clone()) + .unwrap() + .unwrap(), + new_data + ); // Set test entity; as the entity already exists an update should be performed let count = get_entity_count(store.clone(), &TEST_SUBGRAPH_ID); transact_entity_operations( - &store, + &store.subgraph_store(), TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_3_PTR, vec![op], @@ -455,7 +468,10 @@ fn update_existing() { new_data.insert("__typename".to_owned(), USER.into()); new_data.insert("bin_name".to_owned(), Value::Bytes(bin_name)); - assert_eq!(store.get(entity_key).unwrap(), Some(new_data)); + assert_eq!( + store.subgraph_store().get(entity_key).unwrap(), + Some(new_data) + ); }) } @@ -471,13 +487,14 @@ fn partially_update_existing() { ]); let original_entity = store + .subgraph_store() .get(entity_key.clone()) .unwrap() .expect("entity not found"); // Set test entity; as the entity already exists an update should be performed transact_entity_operations( - &store, + &store.subgraph_store(), TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_3_PTR, vec![EntityOperation::Set { @@ -488,7 +505,11 @@ fn partially_update_existing() { .unwrap(); // Obtain the updated entity from the store - let updated_entity = store.get(entity_key).unwrap().expect("entity not found"); + let updated_entity = store + .subgraph_store() + .get(entity_key) + .unwrap() + .expect("entity not found"); // Verify that the values of all attributes we have set were either unset // (in the case of Value::Null) or updated to the new values @@ -518,6 +539,7 @@ impl QueryChecker { let entities = self .store + .subgraph_store() .find(query) .expect("store.find failed to execute query"); @@ -991,16 +1013,20 @@ async fn check_basic_revert( let subscription = subscribe(subgraph_id, entity_type); let state = store + .subgraph_store() .deployment_state_from_id(subgraph_id.to_owned()) + .await .expect("can get deployment state"); assert_eq!(subgraph_id, &state.id); // Revert block 3 store + .subgraph_store() .revert_block_operations(TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_1_PTR) .unwrap(); let returned_entities = store + .subgraph_store() .find(this_query.clone()) .expect("store.find operation failed"); @@ -1014,7 +1040,9 @@ async fn check_basic_revert( assert_eq!(&test_value, returned_name.unwrap()); let state = store + .subgraph_store() .deployment_state_from_id(subgraph_id.to_owned()) + .await .expect("can get deployment state"); assert_eq!(subgraph_id, &state.id); @@ -1051,7 +1079,7 @@ fn revert_block_with_delete() { // Process deletion transact_entity_operations( - &store, + &store.subgraph_store(), TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_3_PTR, vec![EntityOperation::Remove { key: del_key }], @@ -1063,6 +1091,7 @@ fn revert_block_with_delete() { // Revert deletion let count = get_entity_count(store.clone(), &TEST_SUBGRAPH_ID); store + .subgraph_store() .revert_block_operations(TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_2_PTR) .unwrap(); assert_eq!( @@ -1072,6 +1101,7 @@ fn revert_block_with_delete() { // Query after revert let returned_entities = store + .subgraph_store() .find(this_query.clone()) .expect("store.find operation failed"); @@ -1108,13 +1138,14 @@ fn revert_block_with_partial_update() { ]); let original_entity = store + .subgraph_store() .get(entity_key.clone()) .unwrap() .expect("missing entity"); // Set test entity; as the entity already exists an update should be performed transact_entity_operations( - &store, + &store.subgraph_store(), TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_3_PTR, vec![EntityOperation::Set { @@ -1129,12 +1160,14 @@ fn revert_block_with_partial_update() { // Perform revert operation, reversing the partial update let count = get_entity_count(store.clone(), &TEST_SUBGRAPH_ID); store + .subgraph_store() .revert_block_operations(TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_2_PTR) .unwrap(); assert_eq!(count, get_entity_count(store.clone(), &TEST_SUBGRAPH_ID)); // Obtain the reverted entity from the store let reverted_entity = store + .subgraph_store() .get(entity_key.clone()) .unwrap() .expect("missing entity"); @@ -1185,6 +1218,8 @@ fn mock_data_source() -> DataSource { #[test] fn revert_block_with_dynamic_data_source_operations() { run_test(|store| async move { + let store = store.subgraph_store(); + // Create operations to add a user let user_key = EntityKey::data(TEST_SUBGRAPH_ID.clone(), USER.to_owned(), "1".to_owned()); let partial_entity = Entity::from(vec![ @@ -1297,6 +1332,7 @@ fn entity_changes_are_fired_and_forwarded_to_subscriptions() { let name = SubgraphName::new("test/entity-changes-are-fired").unwrap(); let node_id = NodeId::new("test").unwrap(); store + .subgraph_store() .create_subgraph_deployment( name, &schema, @@ -1327,7 +1363,7 @@ fn entity_changes_are_fired_and_forwarded_to_subscriptions() { ), ]; transact_entity_operations( - &store, + &store.subgraph_store(), subgraph_id.clone(), *TEST_BLOCK_1_PTR, added_entities @@ -1357,7 +1393,7 @@ fn entity_changes_are_fired_and_forwarded_to_subscriptions() { // Commit update & delete ops transact_entity_operations( - &store, + &store.subgraph_store(), subgraph_id.clone(), *TEST_BLOCK_2_PTR, vec![update_op, delete_op], @@ -1409,6 +1445,7 @@ fn throttle_subscription_delivers() { store .clone() .query_store(TEST_SUBGRAPH_ID.clone().into(), true) + .await .unwrap(), TEST_SUBGRAPH_ID.clone(), Duration::from_millis(500), @@ -1426,7 +1463,7 @@ fn throttle_subscription_delivers() { ); transact_entity_operations( - &store, + &store.subgraph_store(), TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_3_PTR, vec![user4], @@ -1452,6 +1489,7 @@ fn throttle_subscription_throttles() { store .clone() .query_store(TEST_SUBGRAPH_ID.clone().into(), true) + .await .unwrap(), TEST_SUBGRAPH_ID.clone(), Duration::from_secs(30), @@ -1469,7 +1507,7 @@ fn throttle_subscription_throttles() { ); transact_entity_operations( - &store, + &store.subgraph_store(), TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_3_PTR, vec![user4], @@ -1490,6 +1528,7 @@ fn throttle_subscription_throttles() { fn subgraph_schema_types_have_subgraph_id_directive() { run_test(|store| async move { let schema = store + .subgraph_store() .api_schema(&TEST_SUBGRAPH_ID) .expect("test subgraph should have a schema"); for typedef in schema @@ -1560,6 +1599,7 @@ fn handle_large_string_with_index() { ); store + .subgraph_store() .transact_block_operations( TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_3_PTR, @@ -1581,6 +1621,7 @@ fn handle_large_string_with_index() { .asc(NAME); let ids = store + .subgraph_store() .find(query) .expect("Could not find entity") .iter() @@ -1599,6 +1640,7 @@ fn handle_large_string_with_index() { .asc(NAME); let ids = store + .subgraph_store() .find(query) .expect("Could not find entity") .iter() @@ -1612,7 +1654,7 @@ fn handle_large_string_with_index() { } #[derive(Clone)] -struct WindowQuery(EntityQuery, Arc); +struct WindowQuery(EntityQuery, Arc); impl WindowQuery { fn new(store: &Arc) -> Self { @@ -1620,7 +1662,7 @@ impl WindowQuery { user_query() .filter(EntityFilter::GreaterThan("age".into(), Value::from(0))) .first(10), - store.clone(), + store.subgraph_store(), ) .default_window() } @@ -1756,8 +1798,13 @@ fn window() { ]; run_test(|store| async move { - transact_entity_operations(&store, TEST_SUBGRAPH_ID.clone(), *TEST_BLOCK_3_PTR, ops) - .expect("Failed to create test users"); + transact_entity_operations( + &store.subgraph_store(), + TEST_SUBGRAPH_ID.clone(), + *TEST_BLOCK_3_PTR, + ops, + ) + .expect("Failed to create test users"); // Get the first 2 entries in each 'color group' WindowQuery::new(&store) @@ -1829,6 +1876,7 @@ fn find_at_block() { query.block = block; let entities = store + .subgraph_store() .find(query) .expect("store.find failed to execute query"); @@ -1850,14 +1898,20 @@ fn cleanup_cached_blocks() { // The main purpose for this test is to ensure that the SQL query // we run in `cleanup_cached_blocks` to figure out the first block // that should be removed is syntactically correct - let cleaned = store.cleanup_cached_blocks(10).expect("cleanup succeeds"); + let chain_store = store + .block_store() + .chain_store(NETWORK_NAME) + .expect("fake chain store"); + let cleaned = chain_store + .cleanup_cached_blocks(10) + .expect("cleanup succeeds"); assert_eq!((0, 0), cleaned); }) } #[test] fn reorg_tracking() { - fn update_john(store: &Arc, age: i32, block: &EthereumBlockPointer) { + fn update_john(store: &Arc, age: i32, block: &EthereumBlockPointer) { let test_entity_1 = create_test_entity( "1", USER, @@ -1885,6 +1939,7 @@ fn reorg_tracking() { let subgraph_id = TEST_SUBGRAPH_ID.to_owned(); let state = &$store .deployment_state_from_id(subgraph_id.clone()) + .await .expect("can get deployment state"); assert_eq!(&subgraph_id, &state.id, "subgraph_id"); assert_eq!($reorg_count, state.reorg_count, "reorg_count"); @@ -1899,6 +1954,8 @@ fn reorg_tracking() { // Check that reorg_count, max_reorg_depth, and latest_ethereum_block_number // are reported correctly in DeploymentState run_test(|store| async move { + let store = store.subgraph_store(); + check_state!(store, 0, 0, 2); // Jump to block 4 diff --git a/store/postgres/tests/subgraph.rs b/store/postgres/tests/subgraph.rs index 6574e44766d..1c43a564aa7 100644 --- a/store/postgres/tests/subgraph.rs +++ b/store/postgres/tests/subgraph.rs @@ -1,4 +1,5 @@ use graph::{ + components::store::StatusStore, data::subgraph::schema::MetadataType, data::subgraph::schema::SubgraphError, data::subgraph::schema::SubgraphHealth, @@ -11,10 +12,10 @@ use graph::{ prelude::SubgraphManifest, prelude::SubgraphName, prelude::SubgraphVersionSwitchingMode, - prelude::{CheapClone, NodeId, Store as _, SubgraphDeploymentId}, + prelude::{CheapClone, NodeId, SubgraphDeploymentId, SubgraphStore as _}, }; use graph_store_postgres::layout_for_tests::Connection as Primary; -use graph_store_postgres::NetworkStore; +use graph_store_postgres::SubgraphStore; use std::collections::HashSet; use test_store::*; @@ -53,7 +54,7 @@ fn reassign_subgraph() { id } - fn find_assignment(store: &NetworkStore, id: &SubgraphDeploymentId) -> Option { + fn find_assignment(store: &SubgraphStore, id: &SubgraphDeploymentId) -> Option { store .assigned_node(id) .unwrap() @@ -61,6 +62,8 @@ fn reassign_subgraph() { } run_test_sequentially(setup, |store, id| async move { + let store = store.subgraph_store(); + // Check our setup let node = find_assignment(store.as_ref(), &id); let placement = place("test").expect("the test config places deployments"); @@ -111,7 +114,7 @@ fn create_subgraph() { } fn deploy( - store: &NetworkStore, + store: &SubgraphStore, id: &str, mode: SubgraphVersionSwitchingMode, ) -> HashSet { @@ -166,6 +169,8 @@ fn create_subgraph() { // Test VersionSwitchingMode::Instant run_test_sequentially(remove_subgraphs, |store, _| async move { + let store = store.subgraph_store(); + const MODE: SubgraphVersionSwitchingMode = SubgraphVersionSwitchingMode::Instant; const ID: &str = "instant"; const ID2: &str = "instant2"; @@ -227,6 +232,8 @@ fn create_subgraph() { // Test VersionSwitchingMode::Synced run_test_sequentially(remove_subgraphs, |store, _| async move { + let store = store.subgraph_store(); + const MODE: SubgraphVersionSwitchingMode = SubgraphVersionSwitchingMode::Synced; const ID: &str = "synced"; const ID2: &str = "synced2"; @@ -397,7 +404,11 @@ fn status() { deterministic: true, }; - store.fail_subgraph(id.clone(), error).await.unwrap(); + store + .subgraph_store() + .fail_subgraph(id.clone(), error) + .await + .unwrap(); let infos = store .status(status::Filter::Deployments(vec![id.to_string()])) .unwrap(); @@ -424,7 +435,13 @@ fn version_info() { } run_test_sequentially(setup, |store, id| async move { - transact_entity_operations(&store, id.clone(), BLOCK_ONE.clone(), vec![]).unwrap(); + transact_entity_operations( + &store.subgraph_store(), + id.clone(), + BLOCK_ONE.clone(), + vec![], + ) + .unwrap(); let primary = primary_connection(); let (current, _) = primary.versions_for_subgraph(&*NAME).unwrap(); @@ -444,10 +461,9 @@ fn version_info() { ); assert_eq!(&*NAME, vi.schema.id.as_str()); assert_eq!(Some(1), vi.latest_ethereum_block_number); - // We don't have a data source on the manifest of the test subgraph - // and can therefore not find the network or the head block + assert_eq!(&*NETWORK_NAME, vi.network.as_str()); + // We set the head for the network to null in the test framework assert_eq!(None, vi.total_ethereum_blocks_count); - assert_eq!(None, vi.network); }) } @@ -460,7 +476,7 @@ fn subgraph_error() { test_store::create_test_subgraph(&subgraph_id, "type Foo { id: ID! }"); let count = || -> usize { - let store = store.store(); + let store = store.subgraph_store(); store.error_count(&subgraph_id).unwrap() }; @@ -515,7 +531,10 @@ fn fatal_vs_non_fatal() { } run_test_sequentially(setup, |store, id| async move { - let query_store = store.query_store(id.cheap_clone().into(), false).unwrap(); + let query_store = store + .query_store(id.cheap_clone().into(), false) + .await + .unwrap(); let error = || SubgraphError { subgraph_id: id.clone(), @@ -525,7 +544,11 @@ fn fatal_vs_non_fatal() { deterministic: true, }; - store.fail_subgraph(id.clone(), error()).await.unwrap(); + store + .subgraph_store() + .fail_subgraph(id.clone(), error()) + .await + .unwrap(); assert!(!query_store .has_non_fatal_errors(id.cheap_clone(), None) @@ -540,3 +563,54 @@ fn fatal_vs_non_fatal() { .unwrap()); }) } + +#[test] +fn fail_unfail() { + fn setup() -> SubgraphDeploymentId { + let id = SubgraphDeploymentId::new("failUnfail").unwrap(); + remove_subgraphs(); + create_test_subgraph(&id, SUBGRAPH_GQL); + id + } + + run_test_sequentially(setup, |store, id| async move { + let query_store = store + .query_store(id.cheap_clone().into(), false) + .await + .unwrap(); + + let error = SubgraphError { + subgraph_id: id.clone(), + message: "test".to_string(), + block_ptr: Some(BLOCKS[1]), + handler: None, + deterministic: true, + }; + + store + .subgraph_store() + .fail_subgraph(id.clone(), error) + .await + .unwrap(); + + assert!(!query_store + .has_non_fatal_errors(id.cheap_clone(), None) + .await + .unwrap()); + + // This will unfail the subgraph and delete the fatal error. + store.subgraph_store().unfail(&id).unwrap(); + + // Advance the block ptr to the block of the deleted error. + transact_entity_operations(&store.subgraph_store(), id.cheap_clone(), BLOCKS[1], vec![]) + .unwrap(); + + // We still have no fatal errors. + assert!(!query_store + .has_non_fatal_errors(id.cheap_clone(), None) + .await + .unwrap()); + + test_store::remove_subgraphs(); + }) +} diff --git a/store/test-store/Cargo.toml b/store/test-store/Cargo.toml index 5dd99d659af..87fe25b18bd 100644 --- a/store/test-store/Cargo.toml +++ b/store/test-store/Cargo.toml @@ -12,6 +12,6 @@ graph-mock = { path = "../../mock" } graph-node = { path = "../../node" } graph = { path = "../../graph" } graph-store-postgres = { path = "../postgres" } -lazy_static = "1.1" +lazy_static = "1.4.0" hex-literal = "0.3" diesel = { version = "1.4.5", features = ["postgres", "serde_json", "numeric", "r2d2"] } diff --git a/store/test-store/src/block_store.rs b/store/test-store/src/block_store.rs index d239829f16f..a132d6544ac 100644 --- a/store/test-store/src/block_store.rs +++ b/store/test-store/src/block_store.rs @@ -1,82 +1,16 @@ -use diesel::prelude::*; -use diesel::PgConnection; -use std::str::FromStr; - -use graph::prelude::{serde_json, web3::types::H256, EthereumBlockPointer}; -use graph_store_postgres::db_schema_for_tests as db_schema; use lazy_static::lazy_static; -/// The parts of an Ethereum block that are interesting for these tests: -/// the block number, hash, and the hash of the parent block -#[derive(Clone, Debug, PartialEq)] -pub struct FakeBlock { - pub number: u64, - pub hash: String, - pub parent_hash: String, -} - -impl FakeBlock { - fn make_child(&self, hash: &str) -> Self { - FakeBlock { - number: self.number + 1, - hash: hash.to_owned(), - parent_hash: self.hash.clone(), - } - } - - fn make_no_parent(number: u64, hash: &str) -> Self { - FakeBlock { - number, - hash: hash.to_owned(), - parent_hash: NO_PARENT.clone(), - } - } - - fn insert(&self, conn: &PgConnection) { - use db_schema::ethereum_blocks as b; - - let data = serde_json::to_value(format!( - "{{\"hash\":\"{}\", \"number\":{}}}", - self.hash, self.number - )) - .expect("Failed to serialize block"); - - let errmsg = format!("Failed to insert block {} ({})", self.number, self.hash); - diesel::insert_into(b::table) - .values(( - &b::number.eq(self.number as i64), - &b::hash.eq(&self.hash), - &b::parent_hash.eq(&self.parent_hash), - &b::network_name.eq(super::NETWORK_NAME), - &b::data.eq(data), - )) - .execute(conn) - .expect(&errmsg); - } - - pub fn block_hash(&self) -> H256 { - H256::from_str(self.hash.as_str()).expect("invalid block hash") - } - - pub fn block_ptr(&self) -> EthereumBlockPointer { - EthereumBlockPointer { - number: self.number, - hash: self.block_hash(), - } - } -} - -pub type Chain = Vec<&'static FakeBlock>; +use graph::components::store::BlockStore; +pub use graph_store_postgres::layout_for_tests::chain_support::{ + Chain, FakeBlock, SettableChainStore as _, NO_PARENT, +}; lazy_static! { - // Hash indicating 'no parent' - pub static ref NO_PARENT: String = - "0000000000000000000000000000000000000000000000000000000000000000".to_owned(); // Genesis block pub static ref GENESIS_BLOCK: FakeBlock = FakeBlock { number: super::GENESIS_PTR.number, hash: format!("{:x}", super::GENESIS_PTR.hash), - parent_hash: NO_PARENT.clone() + parent_hash: NO_PARENT.to_string() }; pub static ref BLOCK_ONE: FakeBlock = GENESIS_BLOCK .make_child("8511fa04b64657581e3f00e14543c1d522d5d7e771b54aa3060b662ade47da13"); @@ -96,44 +30,14 @@ lazy_static! { pub static ref BLOCK_SIX_NO_PARENT: FakeBlock = FakeBlock::make_no_parent(6, "6b834521bb753c132fdcf0e1034803ed9068e324112f8750ba93580b393a986b"); } -/// Removes all networks and blocks from the database -pub fn remove() { - use db_schema::ethereum_blocks as b; - use db_schema::ethereum_networks as n; - - crate::store::remove_subgraphs(); - - let conn = super::PRIMARY_POOL - .get() - .expect("Failed to connect to Postgres"); - - diesel::delete(b::table) - .execute(&conn) - .expect("Failed to delete ethereum_blocks"); - diesel::delete(n::table) - .execute(&conn) - .expect("Failed to delete ethereum_networks"); -} - -// Store the given chain as the blocks for the `network` and set the -// network's genesis block to the hash of `GENESIS_BLOCK` -pub fn insert(chain: Chain, network: &str) { - let conn = crate::store::PRIMARY_POOL - .get() - .expect("Failed to connect to Postgres"); - - use db_schema::ethereum_networks as n; - let hash = format!("{:x}", super::GENESIS_PTR.hash); - diesel::insert_into(n::table) - .values(( - &n::name.eq(network), - &n::genesis_block_hash.eq(&hash), - &n::net_version.eq(super::NETWORK_VERSION), - )) - .execute(&conn) - .expect("Failed to insert test network"); +/// Store the given chain as the blocks for the `network` set the +/// network's genesis block to `genesis_hash`, and head block to +/// `null` +pub fn set_chain(chain: Chain, network: &str) { + let store = crate::store::STORE + .block_store() + .chain_store(network) + .unwrap(); - for block in chain { - block.insert(&conn); - } + store.set_chain(&GENESIS_BLOCK.hash, chain); } diff --git a/store/test-store/src/lib.rs b/store/test-store/src/lib.rs index 93dccd1067a..f1769a72f57 100644 --- a/store/test-store/src/lib.rs +++ b/store/test-store/src/lib.rs @@ -1,5 +1,4 @@ #[cfg(debug_assertions)] -#[macro_use] extern crate diesel; #[cfg(debug_assertions)] diff --git a/store/test-store/src/store.rs b/store/test-store/src/store.rs index 442ab726eb5..155007635fe 100644 --- a/store/test-store/src/store.rs +++ b/store/test-store/src/store.rs @@ -4,7 +4,7 @@ use graph::data::query::QueryResults; use graph::data::query::QueryTarget; use graph::data::subgraph::schema::SubgraphError; use graph::log; -use graph::prelude::{QueryStoreManager as _, Store as _, *}; +use graph::prelude::{QueryStoreManager as _, SubgraphStore as _, *}; use graph::{components::store::EntityType, prelude::NodeId}; use graph_graphql::prelude::{ execute_query, Query as PreparedQuery, QueryExecutionOptions, StoreResolver, @@ -12,8 +12,11 @@ use graph_graphql::prelude::{ use graph_mock::MockMetricsRegistry; use graph_node::config::{Config, Opt}; use graph_node::store_builder::StoreBuilder; +use graph_store_postgres::layout_for_tests::FAKE_NETWORK_SHARED; use graph_store_postgres::{connection_pool::ConnectionPool, Shard, SubscriptionManager}; -use graph_store_postgres::{DeploymentPlacer, NetworkStore}; +use graph_store_postgres::{ + BlockStore as DieselBlcokStore, DeploymentPlacer, Store, SubgraphStore as DieselSubgraphStore, +}; use hex_literal::hex; use lazy_static::lazy_static; use std::sync::Mutex; @@ -45,16 +48,15 @@ lazy_static! { Arc::new(MockMetricsRegistry::new()), CONN_POOL_SIZE as usize )); - static ref STORE_POOL_CONFIG: ( - Arc, - ConnectionPool, - Config, - Arc - ) = build_store(); + static ref STORE_POOL_CONFIG: (Arc, ConnectionPool, Config, Arc) = + build_store(); pub(crate) static ref PRIMARY_POOL: ConnectionPool = STORE_POOL_CONFIG.1.clone(); - pub static ref STORE: Arc = STORE_POOL_CONFIG.0.clone(); + pub static ref STORE: Arc = STORE_POOL_CONFIG.0.clone(); static ref CONFIG: Config = STORE_POOL_CONFIG.2.clone(); pub static ref SUBSCRIPTION_MANAGER: Arc = STORE_POOL_CONFIG.3.clone(); + static ref NODE_ID: NodeId = NodeId::new("test").unwrap(); + static ref SUBGRAPH_STORE: Arc = STORE.subgraph_store(); + static ref BLOCK_STORE: Arc = STORE.block_store(); pub static ref GENESIS_PTR: EthereumBlockPointer = ( H256::from(hex!( "bd34884280958002c51d3f7b5f853e6febeba33de0f40d15b0363006533c924f" @@ -95,7 +97,7 @@ lazy_static! { pub fn run_test_sequentially(setup: G, test: F) where G: FnOnce() -> S + Send + 'static, - F: FnOnce(Arc, S) -> R + Send + 'static, + F: FnOnce(Arc, S) -> R + Send + 'static, R: std::future::Future + Send + 'static, { let store = STORE.clone(); @@ -130,7 +132,7 @@ where } pub fn remove_subgraphs() { - STORE + SUBGRAPH_STORE .delete_all_entities_for_test_use_only() .expect("deleting test entities succeeds"); } @@ -165,16 +167,15 @@ fn create_subgraph( name.truncate(32); SubgraphName::new(name).unwrap() }; - let node_id = NodeId::new("test").unwrap(); - STORE.create_deployment_replace( + SUBGRAPH_STORE.create_deployment_replace( name, &schema, deployment, - node_id, + NODE_ID.clone(), NETWORK_NAME.to_string(), SubgraphVersionSwitchingMode::Instant, )?; - STORE.start_subgraph_deployment(&*LOGGER, &subgraph_id) + SUBGRAPH_STORE.start_subgraph_deployment(&*LOGGER, &subgraph_id) } pub fn create_test_subgraph(subgraph_id: &SubgraphDeploymentId, schema: &str) { @@ -187,8 +188,8 @@ pub fn remove_subgraph(id: &SubgraphDeploymentId) { name.truncate(32); SubgraphName::new(name).unwrap() }; - STORE.remove_subgraph(name).unwrap(); - STORE.store().remove_deployment(id).unwrap(); + SUBGRAPH_STORE.remove_subgraph(name).unwrap(); + SUBGRAPH_STORE.remove_deployment(id).unwrap(); } pub fn create_grafted_subgraph( @@ -202,7 +203,7 @@ pub fn create_grafted_subgraph( } pub fn transact_errors( - store: &Arc, + store: &Arc, subgraph_id: SubgraphDeploymentId, block_ptr_to: EthereumBlockPointer, errs: Vec, @@ -213,7 +214,7 @@ pub fn transact_errors( subgraph_id.clone(), metrics_registry.clone(), ); - store.transact_block_operations( + store.subgraph_store().transact_block_operations( subgraph_id, block_ptr_to, Vec::new(), @@ -224,7 +225,7 @@ pub fn transact_errors( /// Convenience to transact EntityOperation instead of EntityModification pub fn transact_entity_operations( - store: &Arc, + store: &Arc, subgraph_id: SubgraphDeploymentId, block_ptr_to: EthereumBlockPointer, ops: Vec, @@ -253,14 +254,9 @@ pub fn transact_entity_operations( pub fn insert_ens_name(hash: &str, name: &str) { use diesel::insert_into; use diesel::prelude::*; - let conn = PRIMARY_POOL.get().unwrap(); + use graph_store_postgres::command_support::catalog::ens_names; - table! { - ens_names(hash) { - hash -> Varchar, - name -> Varchar, - } - } + let conn = PRIMARY_POOL.get().unwrap(); insert_into(ens_names::table) .values((ens_names::hash.eq(hash), ens_names::name.eq(name))) @@ -285,7 +281,7 @@ pub fn insert_entities( }); transact_entity_operations( - &STORE, + &*SUBGRAPH_STORE, subgraph_id.clone(), GENESIS_PTR.clone(), insert_ops.collect::>(), @@ -362,8 +358,8 @@ fn execute_subgraph_query_internal( QueryTarget::Deployment(id) => id, _ => unreachable!("tests do not use this"), }; - let schema = STORE.api_schema(&id).unwrap(); - let network = STORE.network_name(&id).unwrap(); + let schema = SUBGRAPH_STORE.api_schema(&id).unwrap(); + let network = Some(SUBGRAPH_STORE.network_name(&id).unwrap()); let query = return_err!(PreparedQuery::new( &logger, schema, @@ -374,7 +370,9 @@ fn execute_subgraph_query_internal( )); let mut result = QueryResults::empty(); let deployment = query.schema.id().clone(); - let store = STORE.clone().query_store(deployment.into(), false).unwrap(); + let store = rt + .block_on(STORE.clone().query_store(deployment.into(), false)) + .unwrap(); for (bc, (selection_set, error_policy)) in return_err!(query.block_constraint()) { let logger = logger.clone(); let resolver = return_err!(rt.block_on(StoreResolver::at_block( @@ -402,12 +400,7 @@ fn execute_subgraph_query_internal( result } -fn build_store() -> ( - Arc, - ConnectionPool, - Config, - Arc, -) { +fn build_store() -> (Arc, ConnectionPool, Config, Arc) { let mut opt = Opt::default(); let url = std::env::var_os("THEGRAPH_STORE_POSTGRES_DIESEL_URL").filter(|s| s.len() > 0); let file = std::env::var_os("GRAPH_NODE_TEST_CONFIG").filter(|s| s.len() > 0); @@ -429,17 +422,23 @@ fn build_store() -> ( let registry = Arc::new(MockMetricsRegistry::new()); std::thread::spawn(move || { STORE_RUNTIME.lock().unwrap().block_on(async { - let builder = StoreBuilder::new(&*LOGGER, &config, registry); - let net_identifiers = EthereumNetworkIdentifier { + let builder = StoreBuilder::new(&*LOGGER, &*NODE_ID, &config, registry); + let subscription_manager = builder.subscription_manager(); + let primary_pool = builder.primary_pool(); + + let ident = EthereumNetworkIdentifier { net_version: NETWORK_VERSION.to_owned(), genesis_block_hash: GENESIS_PTR.hash, }; ( - builder.network_store(NETWORK_NAME.to_string(), net_identifiers), - builder.primary_pool(), + builder.network_store(vec![ + (NETWORK_NAME.to_string(), ident.clone()), + (FAKE_NETWORK_SHARED.to_string(), ident), + ]), + primary_pool, config, - builder.subscription_manager(), + subscription_manager, ) }) }) @@ -447,7 +446,7 @@ fn build_store() -> ( .unwrap() } -pub fn primary_connection() -> graph_store_postgres::layout_for_tests::Connection { +pub fn primary_connection() -> graph_store_postgres::layout_for_tests::Connection<'static> { let conn = PRIMARY_POOL.get().unwrap(); graph_store_postgres::layout_for_tests::Connection::new(conn) } diff --git a/tests/integration-tests/big-decimal/src/mapping.ts b/tests/integration-tests/big-decimal/src/mapping.ts index 809eceb9ebc..198f22f66b3 100644 --- a/tests/integration-tests/big-decimal/src/mapping.ts +++ b/tests/integration-tests/big-decimal/src/mapping.ts @@ -7,7 +7,7 @@ let one = BigDecimal.fromString("1"); export function handleTrigger(event: Trigger): void { // There are 35 digits after the dot. // big_decimal exponent will be: -35 - 6109 = -6144. - // Minmum exponent is: -6143. + // Minimum exponent is: -6143. // So 1 digit will be removed, the 8, and the 6 will be rounded to 7. let small = BigDecimal.fromString("0.99999999999999999999999999999999968"); @@ -43,4 +43,23 @@ export function handleTrigger(event: Trigger): void { // Test big decimal division. assert(one / BigDecimal.fromString("10") == BigDecimal.fromString("0.1")); + + // Test big int fromString + assert(BigInt.fromString("8888") == BigInt.fromI32(8888)); + + let bigInt = BigInt.fromString("8888888888888888"); + + // Test big int bit or + assert( + (bigInt | BigInt.fromI32(42)) == BigInt.fromString("8888888888888890") + ); + + // Test big int bit and + assert((bigInt & BigInt.fromI32(42)) == BigInt.fromI32(40)); + + // Test big int left shift + assert(bigInt << 6 == BigInt.fromString("568888888888888832")); + + // Test big int right shift + assert(bigInt >> 6 == BigInt.fromString("138888888888888")); } diff --git a/tests/integration-tests/non-fatal-errors/package.json b/tests/integration-tests/non-fatal-errors/package.json index 5fa092319c5..cca8aba8fd6 100644 --- a/tests/integration-tests/non-fatal-errors/package.json +++ b/tests/integration-tests/non-fatal-errors/package.json @@ -5,8 +5,8 @@ "build-contracts": "rm -rf abis bin && solcjs ../common/SimpleContract.sol --abi -o abis && mv abis/*Contract.abi abis/Contract.abi && solcjs ../common/SimpleContract.sol --bin -o bin && mv bin/*Contract.bin bin/Contract.bin", "codegen": "graph codegen", "test": "yarn build-contracts && truffle test --network test", - "create:test": "graph create test/remove-then-update --node http://localhost:18020/", - "deploy:test": "graph deploy test/remove-then-update --ipfs http://localhost:15001/ --node http://localhost:18020/" + "create:test": "graph create test/non-fatal-errors --node http://localhost:18020/", + "deploy:test": "graph deploy test/non-fatal-errors --ipfs http://localhost:15001/ --node http://localhost:18020/" }, "devDependencies": { "@graphprotocol/graph-cli": "https://github.com/graphprotocol/graph-cli#master", diff --git a/tests/integration-tests/non-fatal-errors/test/test.js b/tests/integration-tests/non-fatal-errors/test/test.js index a1c879588d9..c359c559cea 100644 --- a/tests/integration-tests/non-fatal-errors/test/test.js +++ b/tests/integration-tests/non-fatal-errors/test/test.js @@ -11,7 +11,7 @@ const fetchSubgraphs = createApolloFetch({ uri: "http://localhost:18030/graphql", }); const fetchSubgraph = createApolloFetch({ - uri: "http://localhost:18000/subgraphs/name/test/remove-then-update", + uri: "http://localhost:18000/subgraphs/name/test/non-fatal-errors", }); const exec = (cmd) => {