Compare commits

...
Author SHA1 Message Date
Ben 0bed02ed73 trying to get back tracker stats 2024-09-18 12:21:45 +02:00
Ben d03dd9f954 improvements 2024-09-18 12:08:10 +02:00
Ben 9d5abd8955 getting closer 2024-09-17 16:03:58 +02:00
Ben 5b53c1af03 All lined up 2024-09-17 13:44:06 +02:00
Ben 125ee5d22e moves to correct folders 2024-09-17 10:50:52 +02:00
Ben 65da61823a setup 2024-09-17 10:46:38 +02:00
benbierens cedec0d4cc defaults for EC params that actually work 2024-09-12 15:29:50 +02:00
benbierens 769b9c3aca div by zero in time calculation 2024-09-12 15:14:15 +02:00
benbierens 88c675adf9 implements performance logger for autoclient 2024-09-12 15:03:06 +02:00
benbierens 75fcc68caf Merge branch 'feature/better-autoclient' 2024-09-12 14:42:42 +02:00
benbierens a41272f160 Removes rest server 2024-09-12 14:42:19 +02:00
benbierens 8e018cbae9 setup 2024-09-12 14:38:15 +02:00
Ben 3c447eb4c5 multiple nodes on one autoclient 2024-09-12 12:05:42 +02:00
benbierens d53b760731 fixes docker path 2024-09-11 14:26:28 +02:00
benbierens fcadceb009 References autoclientcenter from autoclient 2024-09-11 14:24:12 +02:00
benbierens a02d9558e5 Merge branch 'feature/better-autoclient' 2024-09-11 14:08:29 +02:00
benbierens b3013a9b65 moves project 2024-09-11 14:08:21 +02:00
benbierens 6b0a16b627 Merge branch 'feature/better-autoclient' 2024-09-11 14:07:04 +02:00
benbierens eac06e8b3a builds docker image 2024-09-11 14:06:48 +02:00
benbierens f7fa35c7ba Implements center service 2024-09-11 14:00:22 +02:00
Ben a7526aaed1 setup center api 2024-09-11 12:08:06 +02:00
benbierens e7d9e833f1 Merge branch 'feature/self-updating-contracts-code' 2024-08-30 12:42:24 +02:00
benbierens 13dd0a649c Moves self-updater call to starter class. 2024-08-30 12:25:41 +02:00
benbierens bf5bd8d726 Successful automatic update 2024-08-30 11:17:36 +02:00
benbierens 8dcf9ff15e first implementation of contracts self-updater 2024-08-30 10:58:27 +02:00
benbierens 489209d549 generates deployment base class 2024-08-30 10:27:03 +02:00
Ben 017cee43c0 Merge branch 'index-encoding' 2024-08-27 13:32:46 +02:00
Ben 8fbef6ff51 working runlength implementation and tests 2024-08-27 13:32:33 +02:00
Ben 38ee2e4eb5 run-length encoding wins 2024-08-27 10:16:51 +02:00
Ben e4f7249f1e Update marketplace bytecode 2024-08-26 11:04:23 +02:00
benbierens e3ca516b97 Readme on how to run auto-client 2024-08-23 13:37:25 +02:00
benbierens 88daab379f Allows autoclient to generate files of any size with random data 2024-08-23 13:21:31 +02:00
Ben 00ed3caafe Sets up CI for building marketinsights api 2024-08-21 15:19:11 +02:00
Ben daad6468c6 Removes marketaverages from discordbot and rewarderbot 2024-08-21 15:08:18 +02:00
Ben 3d347a936d Implements marketinsights api 2024-08-21 15:03:20 +02:00
Ben 87bda475df Set up contract-failed event. 2024-08-21 13:59:54 +02:00
Ben 44dfa39737 api plus description 2024-08-21 11:34:54 +02:00
Ben d38f6da26f setup 2024-08-21 11:00:47 +02:00
Ben 5667cfc054 Pyramid test 2024-08-21 10:53:38 +02:00
Ben ecd0e70261 fixes serialization issue of containerAdditionals 2024-08-21 10:45:17 +02:00
Ben 5bbb95f1ff Option to exclude blockReceivedEvents from transcript 2024-08-21 09:53:20 +02:00
Ben 5aa3edbda2 Json formatting for transcript: omit null objects 2024-08-21 09:51:22 +02:00
Ben 9bdebb963b Fixes printing of disttest status 2024-08-20 15:31:45 +02:00
Ben c690598868 Merge branch 'master' into feature/viewing-network-topology
# Conflicts:
#	Tests/CodexTests/BasicTests/MarketplaceTests.cs
2024-08-20 15:26:54 +02:00
Ben ba59ac91d7 Updates marketplace contract 2024-08-20 15:26:19 +02:00
Ben 5170699ae4 implements writers for codex marketplace transcript events 2024-08-20 12:13:42 +02:00
Ben 09d2f418eb Setting up marketplace hooks 2024-08-20 11:44:15 +02:00
Ben cc9d04acd7 Adds normalized kademlia positions 2024-08-14 15:10:33 +02:00
Ben b5c8c55c72 wires up header node identity 2024-08-14 14:13:00 +02:00
benbierens 38b987144e wip: rework identity map 2024-08-13 14:21:15 +02:00
benbierens 8136bf1c92 Includes nodeId as part of node identity in transcript 2024-08-13 13:48:54 +02:00
benbierens 5ddec99114 ensures long-ids for transcript events 2024-08-08 10:24:16 +02:00
benbierens 58cebd43ce Identified issue with nodesDegree overview 2024-08-08 09:39:20 +02:00
benbierens 11a015e0b6 node degree overview 2024-08-07 11:18:40 +02:00
benbierens 745b7e43b4 simple analyser console tool 2024-08-06 15:56:42 +02:00
Ben a72866c725 Fixes issue where full path is included in moment-reference objects 2024-08-05 14:16:44 +02:00
Ben 2ec7f26387 finishes: fast millions of events support 2024-08-05 12:24:57 +02:00
benbierens 72865e7f96 works for 10,000,000 events, within 40 minutes 2024-08-02 17:52:44 +02:00
benbierens 6a0d2830d6 Speeds up transcript building but not by much 2024-08-02 14:25:38 +02:00
benbierens b3710f26ae working but very slow moment aggregation 2024-08-02 10:44:15 +02:00
benbierens f6cd9db408 test passes 2024-08-02 08:56:49 +02:00
benbierens 7a3a2b558b wip 2024-08-01 16:25:28 +02:00
benbierens 53aad5cb37 Sets up event bucket 2024-08-01 14:50:25 +02:00
benbierens fd65e1f022 fixes concurrency issue in http, fixes peer-dropped event 2024-08-01 11:19:05 +02:00
benbierens d16b8cb011 Fixes identity issue for runningpod/runningcontainer and log saving for stopped containers 2024-08-01 10:39:06 +02:00
benbierens 37611bdc66 peer dropped and starting/started events for upload and download 2024-08-01 09:09:30 +02:00
benbierens 7352cdf3fe Adds successful peer-dial event 2024-08-01 08:29:12 +02:00
benbierens 8e084d6ca0 prevents filename clashes 2024-07-31 14:49:42 +02:00
benbierens 017c71a6e8 typo in model 2024-07-31 11:25:59 +02:00
benbierens 488fbb383b Upgrade reader with moment-handler 2024-07-31 11:02:09 +02:00
benbierens cfd635146b Test attribute for creating transcripts 2024-07-31 09:50:02 +02:00
benbierens 7d737a5534 Moves to framework folder 2024-07-30 15:46:16 +02:00
benbierens 0d21246b06 Updates from viewer project 2024-07-30 15:42:51 +02:00
benbierens ec99f5f8aa Ensure peerId is known for all node events 2024-07-29 14:17:54 +02:00
benbierens 9d9f65c5a3 Fixes missing name and null events 2024-07-29 11:02:24 +02:00
benbierens 87271f4f37 Sets up starting event and bootstrap event 2024-07-29 10:16:37 +02:00
benbierens 9bebc23f6a Adds common header 2024-07-29 08:08:17 +02:00
benbierens 682c267b10 wip 2024-07-26 15:19:41 +02:00
benbierens 4de35b579f lock required for transcript writer 2024-07-26 13:21:29 +02:00
benbierens fb4d520ba4 Debugging transcripts for all tests 2024-07-26 10:56:22 +02:00
benbierens 1f72d4f37d Fixes compile of continuous tests 2024-07-26 10:14:54 +02:00
benbierens ecada92dc5 can generate transcript for two-client test 2024-07-26 10:11:29 +02:00
benbierens 410d62849a setting up log converter 2024-07-26 09:14:46 +02:00
benbierens 927ccaa119 successful transcript for two-client test 2024-07-26 08:39:27 +02:00
benbierens d58d65b751 wip connecting to codex plugin 2024-07-25 16:00:51 +02:00
benbierens 802b18e990 working transcript writer and reader 2024-07-25 15:12:25 +02:00
benbierens 8064051f2f Setting up basic idea of overwatch transcript 2024-07-25 10:30:20 +02:00
benbierens 485b2387e4 fixes multipeer download test 2024-07-25 10:10:11 +02:00
Ben ae4d782566 wip 2024-07-24 16:06:45 +02:00
Ben 8f16699bab CodexPluginPrebuild directory travel 2024-07-23 10:59:07 +02:00
Ben 5ba919a638 argument order error 2024-07-23 10:29:01 +02:00
Ben af7aeb2bba update 2024-07-23 09:58:29 +02:00
benbierens 1f11c5d6b5 Fixes custom replacements 2024-07-03 19:11:21 +02:00
Ben 8694bfe9be Merge branch 'feature/auto-client' 2024-07-02 10:44:31 +02:00
Ben 637b5a4736 Very interesting marketplace test 2024-07-02 10:44:15 +02:00
Ben e0cc46260b Adds presistence to log replaces 2024-07-02 10:37:32 +02:00
Ben c1ad97d26f Fixes exception in app-command creation 2024-07-02 10:22:00 +02:00
Ben 1e8f8106b0 Attempt to fix bot commands 2024-07-02 10:08:13 +02:00
Ben 926e323569 Better file manager 2024-07-01 15:59:08 +02:00
benbierens 8d32758918 Add option to delete app commands on startup to discord bot 2024-06-28 13:11:31 +02:00
benbierens 7eed6160eb working as intended 2024-06-28 09:16:12 +02:00
benbierens c57dc4daa1 concurrent purchases 2024-06-28 08:47:09 +02:00
Ben 4c75cebcd6 Implements autoclient with image generator 2024-06-27 15:38:13 +02:00
Ben a820788c7d Merge branch 'master' into feature/auto-client
# Conflicts:
#	cs-codex-dist-testing.sln
2024-06-27 13:42:54 +02:00
Ben 52a02abd3f Fixes incorrect block numbers in eventFormatter 2024-06-27 11:43:25 +02:00
Ben a92455b2a5 bump codex image to v0.1.2 + prover 2024-06-27 11:16:50 +02:00
Ben 933d5e7d4d Merge branch 'better-chain-events' 2024-06-27 11:16:34 +02:00
Ben ba43fd90c6 Adds admin option to set custom replacements 2024-06-27 11:16:17 +02:00
Ben 01ee514c73 Implements known username replacement 2024-06-27 10:44:37 +02:00
Ben 1eb30329c6 Pulls out chain events sender 2024-06-27 10:14:23 +02:00
Ben 0ef55abdf4 Adds events formatter 2024-06-27 10:07:10 +02:00
Ben 8341807d92 Disables role rewards (not configured in real server) 2024-06-26 11:27:19 +02:00
Ben c2712daccb latest images 2024-06-25 13:02:05 +02:00
benbierens 8033da1176 Updates to new Codex image. Fixes tests 2024-06-21 11:29:05 +02:00
benbierens 5afab577a7 Merge branch 'chainstate-update' 2024-06-21 10:19:32 +02:00
benbierens 4be9b9df9a Finished! 2024-06-21 10:19:09 +02:00
benbierens a905f0ce53 fixes crash in market buffer again 2024-06-21 09:59:24 +02:00
benbierens 0c4d3be912 Fixes crash in marketBuffer 2024-06-21 09:33:52 +02:00
benbierens 2b7ba61543 Cleanup 2024-06-21 09:18:00 +02:00
benbierens f9c7e18985 Merge branch 'master' into chainstate-update 2024-06-21 08:56:43 +02:00
benbierens 32e7028029 wip 2024-06-21 08:56:20 +02:00
Ben cc3eddf02d wip 2024-06-20 12:15:59 +02:00
Ben d73ad6db15 Merge branch 'feature/keymaker' 2024-06-19 14:06:20 +02:00
Ben 5280f0732c Sets up docker image for creating keys 2024-06-19 14:04:27 +02:00
Ben 5ca135646a Container crash detection during start-up 2024-06-19 10:39:14 +02:00
Ben e1da38b6e8 Adds test for making sure node does not crash after mysterious CID is downloaded 2024-06-18 11:04:04 +02:00
Ben f7c45d17d7 wip 2024-06-18 09:57:38 +02:00
Ben cb4cdfe69a better logging 2024-06-17 16:00:51 +02:00
Ben bed57dd35b Finish replace of old chainstate object in rewarder bot 2024-06-17 15:34:08 +02:00
benbierens c38a2242ba wip 2024-06-14 11:09:51 +02:00
benbierens d3488dc907 Adds block numbers to chainstate event logging 2024-06-14 10:22:30 +02:00
benbierens 1fa7787d3b Merge branch 'master' into chainstate-update
# Conflicts:
#	ProjectPlugins/CodexPlugin/CodexNode.cs
#	ProjectPlugins/CodexPlugin/MarketplaceAccess.cs
2024-06-14 09:28:34 +02:00
benbierens 54f053cfcc Improves crash detection 2024-06-14 09:05:56 +02:00
Ben 7b5e802efb Applies delete in scalability tests 2024-06-13 08:51:52 +02:00
Ben 390b9de54a Replaces delete with disk info log. 2024-06-12 15:28:08 +02:00
Ben 4fc9835f43 Attempt to log disk space before and after uploads and downloads 2024-06-12 10:48:52 +02:00
Ben 9a46e20b4a Fixes exception in CodexDistTests 2024-06-11 11:33:02 +02:00
Ben 74ca512f92 Sets up test for downloading content by contract CID 2024-06-10 15:15:27 +02:00
Ben 1c856f7615 Fixes log 2024-06-10 14:17:19 +02:00
Ben cc2513bd2f Better chain state representation 2024-06-10 14:04:25 +02:00
Ben 22527a5d93 Deletes block repostore folder in container when Codex node stops. 2024-06-10 10:58:50 +02:00
benbierens 25663b59a0 sets trace level for scalability tests 2024-06-08 10:50:30 +02:00
benbierens aa416d50b3 ensuring enough mounted disk space 2024-06-08 10:36:23 +02:00
benbierens 16bf5ce5ab I think I may be on to something 2024-06-07 18:16:19 +02:00
benbierens 52ae671234 focussing on the interesting numbers for now 2024-06-07 17:08:14 +02:00
benbierens 5ffff1ed07 Disables metrics in scalability test. Downloads container log when retry attempt failed too quickly to be a timeout. 2024-06-07 17:07:35 +02:00
benbierens 18a02b1717 Implements mapping of debug/info routing table nodes 2024-06-07 16:37:31 +02:00
benbierens 3a61fc89c6 Adds WaitForCleanup test attribute to allow tests to wait for resources to be cleaned up 2024-06-06 15:09:52 +02:00
Ben 38c2d1749a Links up status method for checking. 2024-06-06 10:31:31 +02:00
Ben 9900db8d25 simply scalability test failure handling 2024-06-06 10:12:11 +02:00
Ben e53a644718 Updates to new codex image 2024-06-06 10:11:51 +02:00
Ben 62b56e198b Adds logging of node status to base codexdistTest 2024-06-06 09:54:50 +02:00
Ben f4d1dae478 Disables large network scalability test for now 2024-06-05 10:00:50 +02:00
Ben bf75c25152 Cannot assume retry uploads will require as much space as file is large 2024-06-05 09:59:03 +02:00
Ben b888999b61 Adds space check for upload call 2024-06-05 09:34:43 +02:00
Ben e9555cf99e Upload/download failure now automatically investigates node 2024-06-05 09:20:00 +02:00
Ben 11b866986d wip retry upgrade 2024-06-04 11:06:19 +02:00
benbierens 67fc2183b7 Better logging for slot-filled event checking 2024-06-03 12:56:19 +02:00
benbierens 02baa72c1e DebugInfo log 2024-06-03 11:42:52 +02:00
benbierens 383102b988 Adds container name to crash messge. 2024-06-03 11:36:48 +02:00
benbierens 3da36725e3 Updates codex image 2024-06-03 10:58:04 +02:00
benbierens ac7d323201 wip 2024-06-03 10:38:48 +02:00
benbierens a846d51c0c Implements ChainState in CodexContracts plugin 2024-05-31 11:19:50 +02:00
benbierens f5da80dc9c Sets up ChainEvents object 2024-05-31 10:16:57 +02:00
benbierens f94a67adb4 Extracts event methods from CodexContractsAccess 2024-05-31 10:06:34 +02:00
Ben f1d453251c cleanup 2024-05-30 11:33:16 +02:00
Ben 12dc7efd5b wip 2024-05-30 11:17:13 +02:00
Ben 117a30bb82 wip 2024-05-30 10:55:33 +02:00
Ben ccc6c815e4 Logging of entire chain state as seen by rewarder bot 2024-05-29 14:05:16 +02:00
benbierens ba9e4b098f checking that all rewards are sent. 2024-05-24 16:11:51 +02:00
benbierens 69aa3a998f Fixes bot test contract 2024-05-24 15:34:42 +02:00
benbierens 00fd2cebf9 Merge branch 'master' into feature/rewardbot-tests 2024-05-24 14:34:16 +02:00
benbierens 684a99027b nice checks for timings of successful contract 2024-05-24 14:33:59 +02:00
Ben 5143361dcd wip 2024-05-23 11:37:57 +02:00
Ben 6c956c1a64 wip 2024-05-23 11:37:17 +02:00
Ben a749f82ca5 Switches blocktime logs to debug 2024-05-23 11:24:32 +02:00
Ben 6e9ea47b7d Tracking contract state change moments in MarketplaceAccess 2024-05-23 11:15:14 +02:00
Ben b1818400ca Merge branch 'master' into feature/rewardbot-tests
# Conflicts:
#	Tests/CodexTests/UtilityTests/DiscordBotTests.cs
2024-05-22 11:11:47 +02:00
Ben f7bdafbdc5 Improved representation of TestToken amounts 2024-05-22 11:06:34 +02:00
Ben a236544ee9 parameterizing the bot test 2024-05-21 16:10:14 +02:00
Ben e082f8a31c Changes Mint-TT bot parameter to BigInt type. 2024-05-21 13:09:46 +02:00
Ben fa1b560a91 Adds waiting for debug-mode start message 2024-05-21 12:58:17 +02:00
benbierens d6f7e225be successful downloading of bot log 2024-05-20 16:18:01 +02:00
Ben 22cf82b99b Adds no-discord debug option to discord bot. 2024-05-16 16:00:19 +02:00
Ben ad2181db0b Fixes invariant-culture in assigner. Adds support for ulong. 2024-05-16 14:58:42 +02:00
Ben 3525d13e69 Merge branch 'feature/bot-use-ids' 2024-05-16 14:07:29 +02:00
Ben 8847de116d cleanup rewarder bot config 2024-05-16 14:07:14 +02:00
Ben 7d9dcb263d Prints default ArgsUniform values. Applies discord IDs instead of names. 2024-05-16 14:05:58 +02:00
Ben b3771cce32 Extract assigner from argsUniform 2024-05-16 12:14:03 +02:00
Ben 9717591224 Updates bot config to use ids instead of names 2024-05-16 11:54:31 +02:00
Ben 64ae7c8efe Latest image of Codex 2024-05-15 10:34:40 +02:00
benbierens d8d7605ce5 Increases marketplace graceperiod timespan. 2024-05-10 10:08:45 +02:00
benbierens 6995cbfb23 hacky way to download container logs while retries are failing. 2024-05-09 09:38:04 +02:00
benbierens c6a757d6fb Rename AddCodex. Better retry logging 2024-05-09 09:32:48 +02:00
benbierens c01f9dbb21 Fixes crash in blocktimefinder. 2024-05-09 08:57:19 +02:00
Ben 0ec43a9325 Shuts up json-serialization codex logging 2024-05-07 11:04:32 +02:00
Ben 266c661958 Updates codex API 2024-05-07 10:21:05 +02:00
Ben 8ea4c4ee37 Clean up codex long tests 2024-05-07 10:15:02 +02:00
Ben 725dfc23a6 Cleanup of codex tests 2024-05-07 09:49:00 +02:00
Ben 39ff757b39 Update + instructions for codex contracts 2024-05-06 16:12:26 +02:00
benbierens e187bfc941 Changes time.retry to fixed timelength instead of fixed number of retries 2024-05-02 08:41:20 +02:00
benbierens dd36929a81 Sets scalability tests to current codex image 2024-05-01 13:51:46 +02:00
Ben 2902f6baab latest master image of Codex 2024-05-01 10:05:46 +02:00
benbierens f38f7861a0 Log comparing test 2024-04-27 18:08:54 +02:00
benbierens 84a68521b0 sets up parser for codex log lines 2024-04-27 14:20:31 +02:00
benbierens ce7ba47a32 trying to detect upload slowdown related to number of blocks in node 2024-04-27 11:33:13 +02:00
Ben a20e9a30cf bumps to image with proof support 2024-04-26 11:10:14 +02:00
Ben 52cec0e9f3 bump codex image 2024-04-26 10:43:36 +02:00
Ben 9a6866cf8e clean up logs in Codex 2024-04-25 15:04:02 +02:00
Ben 15d7e6483e Allows parsing longs only from a certain timestamp 2024-04-25 10:57:19 +02:00
Ben e1c710a093 Merge branch 'less-announce-tests' 2024-04-25 10:25:29 +02:00
Ben 0c700ded9d Test for detecting common log lines 2024-04-25 10:25:12 +02:00
benbierens 362040bd3c Sets up docker image and CI 2024-04-01 21:29:02 +02:00
benbierens 40393c3a5b Using raw version of purchase-status call. API doesn't line up 2024-04-01 21:09:24 +02:00
benbierens 7f972bac85 Mvoes autoclient 2024-04-01 20:47:56 +02:00
benbierens d532d9505a Sets up autoclient. 2024-04-01 20:40:03 +02:00
256 changed files with 11068 additions and 2673 deletions
+30
View File
@@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**
+26
View File
@@ -0,0 +1,26 @@
name: Docker - AutoClient
on:
push:
branches:
- master
tags:
- 'v*.*.*'
paths:
- 'Tools/AutoClient/**'
- '!Tools/AutoClient/docker/docker-compose.yaml'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-autoclient.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
with:
docker_file: Tools/AutoClient/docker/Dockerfile
docker_repo: codexstorage/codex-autoclient
secrets: inherit
+27
View File
@@ -0,0 +1,27 @@
name: Docker - KeyMaker
on:
push:
branches:
- master
tags:
- 'v*.*.*'
paths:
- 'Tools/KeyMaker/**'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-KeyMaker.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
with:
docker_file: Tools/KeyMaker/docker/Dockerfile
docker_repo: codexstorage/codex-keymaker
secrets: inherit
@@ -0,0 +1,27 @@
name: Docker - MarketInsights API
on:
push:
branches:
- master
tags:
- 'v*.*.*'
paths:
- 'Tools/MarketInsights/**'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-marketinsights.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
with:
docker_file: Tools/MarketInsights/Dockerfile
docker_repo: codexstorage/codex-marketinsights
secrets: inherit
+2 -1
View File
@@ -1,4 +1,5 @@
.vs
obj
bin
.vscode
.vscode
Tools/AutoClient/datapath
+33 -158
View File
@@ -4,9 +4,8 @@ namespace ArgsUniform
{
public class ArgsUniform<T>
{
private readonly Assigner<T> assigner;
private readonly Action printAppInfo;
private readonly object? defaultsProvider;
private readonly IEnv.IEnv env;
private readonly string[] args;
private const int cliStart = 8;
private const int shortStart = 38;
@@ -31,9 +30,9 @@ namespace ArgsUniform
public ArgsUniform(Action printAppInfo, object defaultsProvider, IEnv.IEnv env, params string[] args)
{
this.printAppInfo = printAppInfo;
this.defaultsProvider = defaultsProvider;
this.env = env;
this.args = args;
assigner = new Assigner<T>(env, args, defaultsProvider);
}
public T Parse(bool printResult = false)
@@ -42,7 +41,7 @@ namespace ArgsUniform
{
printAppInfo();
PrintHelp();
throw new Exception();
Environment.Exit(0);
}
var result = Activator.CreateInstance<T>();
@@ -53,18 +52,16 @@ namespace ArgsUniform
var attr = uniformProperty.GetCustomAttribute<UniformAttribute>();
if (attr != null)
{
if (!UniformAssign(result, attr, uniformProperty) && attr.Required)
if (!assigner.UniformAssign(result, attr, uniformProperty) && attr.Required)
{
{
missingRequired.Add(uniformProperty);
}
missingRequired.Add(uniformProperty);
}
}
}
if (missingRequired.Any())
{
PrintResults(result, uniformProperties);
PrintResults(printResult,result, uniformProperties);
Print("");
foreach (var missing in missingRequired)
{
@@ -75,37 +72,39 @@ namespace ArgsUniform
}
PrintHelp();
throw new ArgumentException("Unable to assemble all required arguments");
Environment.Exit(1);
}
if (printResult)
{
PrintResults(result, uniformProperties);
}
PrintResults(printResult, result, uniformProperties);
return result;
}
private void PrintResults(T result, PropertyInfo[] uniformProperties)
{
Print("");
foreach (var p in uniformProperties)
{
Print($"\t{p.Name} = {p.GetValue(result)}");
}
Print("");
}
public void PrintHelp()
{
Print("");
PrintAligned("CLI option:", "(short)", "Environment variable:", "Description");
var attrs = typeof(T).GetProperties().Where(m => m.GetCustomAttributes(typeof(UniformAttribute), false).Length == 1).Select(p => p.GetCustomAttribute<UniformAttribute>()).Where(a => a != null).ToArray();
foreach (var attr in attrs)
PrintAligned("CLI option:", "(short)", "Environment variable:", "Description", "(default)");
var props = typeof(T).GetProperties().Where(m => m.GetCustomAttributes(typeof(UniformAttribute), false).Length == 1).ToArray();
foreach (var prop in props)
{
var a = attr!;
var optional = !a.Required ? " *" : "";
PrintAligned($"--{a.Arg}=...", $"({a.ArgShort})", a.EnvVar, a.Description + optional);
var a = prop.GetCustomAttribute<UniformAttribute>();
if (a != null)
{
var optional = !a.Required ? " (optional)" : "";
var def = assigner.DescribeDefaultFor(prop);
PrintAligned($"--{a.Arg}=...", $"({a.ArgShort})", a.EnvVar, a.Description + optional, $"({def})");
}
}
Print("");
}
private void PrintResults(bool printResult, T result, PropertyInfo[] uniformProperties)
{
if (!printResult) return;
Print("");
foreach (var p in uniformProperties)
{
Print($"\t{p.Name} = {p.GetValue(result)}");
}
Print("");
}
@@ -115,7 +114,7 @@ namespace ArgsUniform
Console.WriteLine(msg);
}
private void PrintAligned(string cli, string s, string env, string desc)
private void PrintAligned(string cli, string s, string env, string desc, string def)
{
Console.CursorLeft = cliStart;
Console.Write(cli);
@@ -124,132 +123,8 @@ namespace ArgsUniform
Console.CursorLeft = envStart;
Console.Write(env);
Console.CursorLeft = descStart;
Console.Write(desc + Environment.NewLine);
}
private object GetDefaultValue(Type t)
{
if (t.IsValueType) return Activator.CreateInstance(t)!;
return null!;
}
private bool UniformAssign(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
if (AssignFromArgsIfAble(result, attr, uniformProperty)) return true;
if (AssignFromEnvVarIfAble(result, attr, uniformProperty)) return true;
if (AssignFromDefaultsIfAble(result, uniformProperty)) return true;
return false;
}
private bool AssignFromDefaultsIfAble(T result, PropertyInfo uniformProperty)
{
var currentValue = uniformProperty.GetValue(result);
var isEmptryString = (currentValue as string) == string.Empty;
if (currentValue != GetDefaultValue(uniformProperty.PropertyType) && !isEmptryString) return true;
if (defaultsProvider == null) return false;
var defaultProperty = defaultsProvider.GetType().GetProperties().SingleOrDefault(p => p.Name == uniformProperty.Name);
if (defaultProperty == null) return false;
var value = defaultProperty.GetValue(defaultsProvider);
if (value != null)
{
return Assign(result, uniformProperty, value);
}
return false;
}
private bool AssignFromEnvVarIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
var e = env.GetEnvVarOrDefault(attr.EnvVar, string.Empty);
if (!string.IsNullOrEmpty(e))
{
return Assign(result, uniformProperty, e);
}
return false;
}
private bool AssignFromArgsIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
var fromArg = GetFromArgs(attr.Arg);
if (fromArg != null)
{
return Assign(result, uniformProperty, fromArg);
}
var fromShort = GetFromArgs(attr.ArgShort);
if (fromShort != null)
{
return Assign(result, uniformProperty, fromShort);
}
return false;
}
private bool Assign(T result, PropertyInfo uniformProperty, object value)
{
if (uniformProperty.PropertyType == value.GetType())
{
uniformProperty.SetValue(result, value);
return true;
}
else
{
if (uniformProperty.PropertyType == typeof(string) || uniformProperty.PropertyType == typeof(int))
{
uniformProperty.SetValue(result, Convert.ChangeType(value, uniformProperty.PropertyType));
return true;
}
else
{
if (uniformProperty.PropertyType == typeof(int?)) return AssignOptionalInt(result, uniformProperty, value);
if (uniformProperty.PropertyType.IsEnum) return AssignEnum(result, uniformProperty, value);
if (uniformProperty.PropertyType == typeof(bool)) return AssignBool(result, uniformProperty, value);
throw new NotSupportedException();
}
}
}
private static bool AssignEnum(T result, PropertyInfo uniformProperty, object value)
{
var s = value.ToString();
if (Enum.TryParse(uniformProperty.PropertyType, s, out var e))
{
uniformProperty.SetValue(result, e);
return true;
}
return false;
}
private static bool AssignOptionalInt(T result, PropertyInfo uniformProperty, object value)
{
if (int.TryParse(value.ToString(), out int i))
{
uniformProperty.SetValue(result, i);
return true;
}
return false;
}
private static bool AssignBool(T result, PropertyInfo uniformProperty, object value)
{
var s = value.ToString();
if (s == "1" || (s != null && s.ToLowerInvariant() == "true"))
{
uniformProperty.SetValue(result, true);
}
return true;
}
private string? GetFromArgs(string key)
{
var argKey = $"--{key}=";
var arg = args.FirstOrDefault(a => a.StartsWith(argKey));
if (arg != null)
{
return arg.Substring(argKey.Length);
}
return null;
Console.Write(desc + " ");
Console.Write(def + Environment.NewLine);
}
}
}
+186
View File
@@ -0,0 +1,186 @@
using System.Globalization;
using System.Numerics;
using System.Reflection;
namespace ArgsUniform
{
public class Assigner<T>
{
private readonly IEnv.IEnv env;
private readonly string[] args;
private readonly object? defaultsProvider;
public Assigner(IEnv.IEnv env, string[] args, object? defaultsProvider)
{
this.env = env;
this.args = args;
this.defaultsProvider = defaultsProvider;
}
public bool UniformAssign(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
if (AssignFromArgsIfAble(result, attr, uniformProperty)) return true;
if (AssignFromEnvVarIfAble(result, attr, uniformProperty)) return true;
if (AssignFromDefaultsIfAble(result, uniformProperty)) return true;
return false;
}
public string DescribeDefaultFor(PropertyInfo property)
{
var obj = Activator.CreateInstance<T>();
var defaultValue = GetDefaultValue(obj, property);
if (defaultValue == null) return "";
if (defaultValue is string str)
{
return "\"" + str + "\"";
}
return defaultValue.ToString() ?? string.Empty;
}
private object? GetDefaultValue(T result, PropertyInfo uniformProperty)
{
// Get value from object's static initializer if it's there.
var currentValue = uniformProperty.GetValue(result);
if (currentValue != null) return currentValue;
// Get value from defaults-provider object if it's there.
if (defaultsProvider == null) return null;
var defaultProperty = defaultsProvider.GetType().GetProperties().SingleOrDefault(p => p.Name == uniformProperty.Name);
if (defaultProperty == null) return null;
return defaultProperty.GetValue(defaultsProvider);
}
private bool AssignFromDefaultsIfAble(T result, PropertyInfo uniformProperty)
{
var defaultValue = GetDefaultValue(result, uniformProperty);
var isEmptryString = (defaultValue as string) == string.Empty;
if (defaultValue != null && defaultValue != GetDefaultValueForType(uniformProperty.PropertyType) && !isEmptryString)
{
return Assign(result, uniformProperty, defaultValue);
}
return false;
}
private bool AssignFromEnvVarIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
var e = env.GetEnvVarOrDefault(attr.EnvVar, string.Empty);
if (!string.IsNullOrEmpty(e))
{
return Assign(result, uniformProperty, e);
}
return false;
}
private bool AssignFromArgsIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
var fromArg = GetFromArgs(attr.Arg);
if (fromArg != null)
{
return Assign(result, uniformProperty, fromArg);
}
var fromShort = GetFromArgs(attr.ArgShort);
if (fromShort != null)
{
return Assign(result, uniformProperty, fromShort);
}
return false;
}
private bool Assign(T result, PropertyInfo uniformProperty, object value)
{
if (uniformProperty.PropertyType == value.GetType())
{
uniformProperty.SetValue(result, value);
return true;
}
else
{
if (uniformProperty.PropertyType == typeof(string) || uniformProperty.PropertyType == typeof(int))
{
uniformProperty.SetValue(result, Convert.ChangeType(value, uniformProperty.PropertyType));
return true;
}
else
{
if (uniformProperty.PropertyType == typeof(int?)) return AssignOptionalInt(result, uniformProperty, value);
if (uniformProperty.PropertyType.IsEnum) return AssignEnum(result, uniformProperty, value);
if (uniformProperty.PropertyType == typeof(bool)) return AssignBool(result, uniformProperty, value);
if (uniformProperty.PropertyType == typeof(ulong)) return AssignUlong(result, uniformProperty, value);
if (uniformProperty.PropertyType == typeof(BigInteger)) return AssignBigInt(result, uniformProperty, value);
throw new NotSupportedException(
$"Unsupported property type '${uniformProperty.PropertyType}' " +
$"for property '${uniformProperty.Name}'.");
}
}
}
private static bool AssignEnum(T result, PropertyInfo uniformProperty, object value)
{
var s = value.ToString();
if (Enum.TryParse(uniformProperty.PropertyType, s, out var e))
{
uniformProperty.SetValue(result, e);
return true;
}
return false;
}
private static bool AssignOptionalInt(T result, PropertyInfo uniformProperty, object value)
{
if (int.TryParse(value.ToString(), CultureInfo.InvariantCulture, out int i))
{
uniformProperty.SetValue(result, i);
return true;
}
return false;
}
private bool AssignUlong(T? result, PropertyInfo uniformProperty, object value)
{
if (ulong.TryParse(value.ToString(), CultureInfo.InvariantCulture, out ulong i))
{
uniformProperty.SetValue(result, i);
return true;
}
return false;
}
private bool AssignBigInt(T result, PropertyInfo uniformProperty, object value)
{
if (BigInteger.TryParse(value.ToString(), CultureInfo.InvariantCulture, out BigInteger i))
{
uniformProperty.SetValue(result, i);
return true;
}
return false;
}
private static bool AssignBool(T result, PropertyInfo uniformProperty, object value)
{
var s = value.ToString();
if (s == "1" || (s != null && s.ToLowerInvariant() == "true"))
{
uniformProperty.SetValue(result, true);
}
return true;
}
private string? GetFromArgs(string key)
{
var argKey = $"--{key}=";
var arg = args.FirstOrDefault(a => a.StartsWith(argKey));
if (arg != null)
{
return arg.Substring(argKey.Length);
}
return null;
}
private static object GetDefaultValueForType(Type t)
{
if (t.IsValueType) return Activator.CreateInstance(t)!;
return null!;
}
}
}
+1 -5
View File
@@ -30,11 +30,7 @@ namespace Core
public IDownloadedLog DownloadLog(RunningContainer container, int? tailLines = null)
{
var workflow = entryPoint.Tools.CreateWorkflow();
var file = entryPoint.Tools.GetLog().CreateSubfile();
entryPoint.Tools.GetLog().Log($"Downloading container log for '{container.Name}' to file '{file.FullFilename}'...");
var logHandler = new LogDownloadHandler(container.Name, file);
workflow.DownloadContainerLog(container, logHandler, tailLines);
return logHandler.DownloadLog();
return workflow.DownloadContainerLog(container, tailLines);
}
public string ExecuteContainerCommand(IHasContainer containerSource, string command, params string[] args)
+11
View File
@@ -16,6 +16,7 @@ namespace Core
TResponse HttpPostString<TResponse>(string route, string body);
string HttpPostStream(string route, Stream stream);
Stream HttpGetStream(string route);
string HttpPutString(string route, string body);
T Deserialize<T>(string json);
}
@@ -114,6 +115,16 @@ namespace Core
}, $"HTTP-GET-STREAM: {route}");
}
public string HttpPutString(string route, string body)
{
return http.OnClient(client =>
{
var response = Time.Wait(client.PutAsync(GetUrl() + route,
new StringContent(body, MediaTypeHeaderValue.Parse("application/json"))));
return Time.Wait(response.Content.ReadAsStringAsync());
}, $"HTTP-PUT-STR: {route}");
}
public T Deserialize<T>(string json)
{
var errors = new List<string>();
+7 -3
View File
@@ -38,10 +38,14 @@ namespace Core
return new CoreInterface(this);
}
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles)
/// <summary>
/// Deletes kubernetes and tracked file resources.
/// when `waitTillDone` is true, this function will block until resources are deleted.
/// </summary>
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone)
{
manager.DecommissionPlugins(deleteKubernetesResources, deleteTrackedFiles);
Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles);
manager.DecommissionPlugins(deleteKubernetesResources, deleteTrackedFiles, waitTillDone);
Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles, waitTillDone);
}
internal T GetPlugin<T>() where T : IProjectPlugin
+27 -7
View File
@@ -7,23 +7,27 @@ namespace Core
{
T OnClient<T>(Func<HttpClient, T> action);
T OnClient<T>(Func<HttpClient, T> action, string description);
T OnClient<T>(Func<HttpClient, T> action, Retry retry);
IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null);
}
internal class Http : IHttp
{
private static readonly object httpLock = new object();
private static object lockLock = new object();
private static readonly Dictionary<string, object> httpLocks = new Dictionary<string, object>();
private readonly ILog log;
private readonly ITimeSet timeSet;
private readonly Action<HttpClient> onClientCreated;
private readonly string id;
internal Http(ILog log, ITimeSet timeSet)
: this(log, timeSet, DoNothing)
internal Http(string id, ILog log, ITimeSet timeSet)
: this(id, log, timeSet, DoNothing)
{
}
internal Http(ILog log, ITimeSet timeSet, Action<HttpClient> onClientCreated)
internal Http(string id, ILog log, ITimeSet timeSet, Action<HttpClient> onClientCreated)
{
this.id = id;
this.log = log;
this.timeSet = timeSet;
this.onClientCreated = onClientCreated;
@@ -35,13 +39,19 @@ namespace Core
}
public T OnClient<T>(Func<HttpClient, T> action, string description)
{
var retry = new Retry(description, timeSet.HttpRetryTimeout(), timeSet.HttpCallRetryDelay(), f => { });
return OnClient(action, retry);
}
public T OnClient<T>(Func<HttpClient, T> action, Retry retry)
{
var client = GetClient();
return LockRetry(() =>
{
return action(client);
}, description);
}, retry);
}
public IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null)
@@ -54,11 +64,21 @@ namespace Core
return DebugStack.GetCallerName(skipFrames: 2);
}
private T LockRetry<T>(Func<T> operation, string description)
private T LockRetry<T>(Func<T> operation, Retry retry)
{
var httpLock = GetLock();
lock (httpLock)
{
return Time.Retry(operation, timeSet.HttpMaxNumberOfRetries(), timeSet.HttpCallRetryDelay(), description);
return retry.Run(operation);
}
}
private object GetLock()
{
lock (lockLock) // I had to.
{
if (!httpLocks.ContainsKey(id)) httpLocks.Add(id, new object());
return httpLocks[id];
}
}
-28
View File
@@ -1,28 +0,0 @@
using KubernetesWorkflow;
using Logging;
namespace Core
{
internal class LogDownloadHandler : LogHandler, ILogHandler
{
private readonly LogFile log;
internal LogDownloadHandler(string description, LogFile log)
{
this.log = log;
log.Write($"{description} -->> {log.FullFilename}");
log.WriteRaw(description);
}
internal IDownloadedLog DownloadLog()
{
return new DownloadedLog(log);
}
protected override void ProcessLine(string line)
{
log.WriteRaw(line);
}
}
}
+2 -2
View File
@@ -34,12 +34,12 @@
return metadata;
}
internal void DecommissionPlugins(bool deleteKubernetesResources, bool deleteTrackedFiles)
internal void DecommissionPlugins(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone)
{
foreach (var pair in pairs)
{
pair.Plugin.Decommission();
pair.Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles);
pair.Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles, waitTillDone);
}
}
+21 -14
View File
@@ -6,7 +6,13 @@ namespace Core
{
public interface IPluginTools : IWorkflowTool, ILogTool, IHttpFactoryTool, IFileTool
{
void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles);
ITimeSet TimeSet { get; }
/// <summary>
/// Deletes kubernetes and tracked file resources.
/// when `waitTillDone` is true, this function will block until resources are deleted.
/// </summary>
void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone);
}
public interface IWorkflowTool
@@ -21,9 +27,9 @@ namespace Core
public interface IHttpFactoryTool
{
IHttp CreateHttp(Action<HttpClient> onClientCreated);
IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet timeSet);
IHttp CreateHttp();
IHttp CreateHttp(string id, Action<HttpClient> onClientCreated);
IHttp CreateHttp(string id, Action<HttpClient> onClientCreated, ITimeSet timeSet);
IHttp CreateHttp(string id);
}
public interface IFileTool
@@ -33,7 +39,6 @@ namespace Core
internal class PluginTools : IPluginTools
{
private readonly ITimeSet timeSet;
private readonly WorkflowCreator workflowCreator;
private readonly IFileManager fileManager;
private readonly LogPrefixer log;
@@ -42,28 +47,30 @@ namespace Core
{
this.log = new LogPrefixer(log);
this.workflowCreator = workflowCreator;
this.timeSet = timeSet;
TimeSet = timeSet;
fileManager = new FileManager(log, fileManagerRootFolder);
}
public ITimeSet TimeSet { get; }
public void ApplyLogPrefix(string prefix)
{
log.Prefix = prefix;
}
public IHttp CreateHttp(Action<HttpClient> onClientCreated)
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated)
{
return CreateHttp(onClientCreated, timeSet);
return CreateHttp(id, onClientCreated, TimeSet);
}
public IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet ts)
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated, ITimeSet ts)
{
return new Http(log, ts, onClientCreated);
return new Http(id, log, ts, onClientCreated);
}
public IHttp CreateHttp()
public IHttp CreateHttp(string id)
{
return new Http(log, timeSet);
return new Http(id, log, TimeSet);
}
public IStartupWorkflow CreateWorkflow(string? namespaceOverride = null)
@@ -71,9 +78,9 @@ namespace Core
return workflowCreator.CreateWorkflow(namespaceOverride);
}
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles)
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone)
{
if (deleteKubernetesResources) CreateWorkflow().DeleteNamespace();
if (deleteKubernetesResources) CreateWorkflow().DeleteNamespace(waitTillDone);
if (deleteTrackedFiles) fileManager.DeleteAllFiles();
}
+29 -8
View File
@@ -2,10 +2,31 @@
{
public interface ITimeSet
{
/// <summary>
/// Timeout for a single HTTP call.
/// </summary>
TimeSpan HttpCallTimeout();
int HttpMaxNumberOfRetries();
/// <summary>
/// Maximum total time to attempt to make a successful HTTP call to a service.
/// When HTTP calls time out during this timespan, retries will be made.
/// </summary>
TimeSpan HttpRetryTimeout();
/// <summary>
/// After a failed HTTP call, wait this long before trying again.
/// </summary>
TimeSpan HttpCallRetryDelay();
/// <summary>
/// After a failed K8s operation, wait this long before trying again.
/// </summary>
TimeSpan K8sOperationRetryDelay();
/// <summary>
/// Maximum total time to attempt to perform a successful k8s operation.
/// If k8s operations fail during this timespan, retries will be made.
/// </summary>
TimeSpan K8sOperationTimeout();
}
@@ -13,12 +34,12 @@
{
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromMinutes(3);
return TimeSpan.FromMinutes(2);
}
public int HttpMaxNumberOfRetries()
public TimeSpan HttpRetryTimeout()
{
return 3;
return TimeSpan.FromMinutes(5);
}
public TimeSpan HttpCallRetryDelay()
@@ -41,17 +62,17 @@
{
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromHours(2);
return TimeSpan.FromMinutes(30);
}
public int HttpMaxNumberOfRetries()
public TimeSpan HttpRetryTimeout()
{
return 1;
return TimeSpan.FromHours(2.2);
}
public TimeSpan HttpCallRetryDelay()
{
return TimeSpan.FromSeconds(2);
return TimeSpan.FromSeconds(20);
}
public TimeSpan K8sOperationRetryDelay()
+4 -4
View File
@@ -13,9 +13,9 @@ namespace DiscordRewards
public enum CheckType
{
Uninitialized,
FilledSlot,
FinishedSlot,
PostedContract,
StartedContract,
HostFilledSlot,
HostFinishedSlot,
ClientPostedContract,
ClientStartedContract,
}
}
+5 -12
View File
@@ -3,8 +3,12 @@
public class GiveRewardsCommand
{
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
public MarketAverage[] Averages { get; set; } = Array.Empty<MarketAverage>();
public string[] EventsOverview { get; set; } = Array.Empty<string>();
public bool HasAny()
{
return Rewards.Any() || EventsOverview.Any();
}
}
public class RewardUsersCommand
@@ -12,15 +16,4 @@
public ulong RewardId { get; set; }
public string[] UserAddresses { get; set; } = Array.Empty<string>();
}
public class MarketAverage
{
public int NumberOfFinished { get; set; }
public int TimeRangeSeconds { get; set; }
public float Price { get; set; }
public float Size { get; set; }
public float Duration { get; set; }
public float Collateral { get; set; }
public float ProofProbability { get; set; }
}
}
+41 -41
View File
@@ -1,53 +1,53 @@
using Utils;
namespace DiscordRewards
namespace DiscordRewards
{
public class RewardRepo
{
private static string Tag => RewardConfig.UsernameTag;
public RewardConfig[] Rewards { get; } = new RewardConfig[]
{
// Filled any slot
new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
{
Type = CheckType.FilledSlot
}),
public RewardConfig[] Rewards { get; } = new RewardConfig[0];
// Finished any slot
new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
{
Type = CheckType.FinishedSlot
}),
// Example configuration, from test server:
//{
// // Filled any slot
// new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
// {
// Type = CheckType.HostFilledSlot
// }),
// Finished a sizable slot
new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig
{
Type = CheckType.FinishedSlot,
MinSlotSize = 10.MB(),
MinDuration = TimeSpan.FromMinutes(5.0),
}),
// // Finished any slot
// new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
// {
// Type = CheckType.HostFinishedSlot
// }),
// Posted any contract
new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
{
Type = CheckType.PostedContract
}),
// // Finished a sizable slot
// new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig
// {
// Type = CheckType.HostFinishedSlot,
// MinSlotSize = 10.MB(),
// MinDuration = TimeSpan.FromMinutes(5.0),
// }),
// Started any contract
new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
{
Type = CheckType.StartedContract
}),
// // Posted any contract
// new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
// {
// Type = CheckType.ClientPostedContract
// }),
// Started a sizable contract
new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig
{
Type = CheckType.StartedContract,
MinNumberOfHosts = 4,
MinSlotSize = 10.MB(),
MinDuration = TimeSpan.FromMinutes(5.0),
})
};
// // Started any contract
// new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
// {
// Type = CheckType.ClientStartedContract
// }),
// // Started a sizable contract
// new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig
// {
// Type = CheckType.ClientStartedContract,
// MinNumberOfHosts = 4,
// MinSlotSize = 10.MB(),
// MinDuration = TimeSpan.FromMinutes(5.0),
// })
//};
}
}
+60 -48
View File
@@ -7,27 +7,42 @@ namespace FileUtils
{
TrackedFile CreateEmptyFile(string label = "");
TrackedFile GenerateFile(ByteSize size, string label = "");
TrackedFile GenerateFile(Action<IGenerateOption> options, string label = "");
void DeleteAllFiles();
void ScopedFiles(Action action);
T ScopedFiles<T>(Func<T> action);
}
public interface IGenerateOption
{
IGenerateOption Random(ByteSize size);
IGenerateOption StringRepeat(string str, ByteSize size);
IGenerateOption StringRepeat(string str, int times);
IGenerateOption ByteRepeat(byte[] bytes, ByteSize size);
IGenerateOption ByteRepeat(byte[] bytes, int times);
}
public class FileManager : IFileManager
{
public const int ChunkSize = 1024 * 1024 * 100;
private static NumberSource folderNumberSource = new NumberSource(0);
private readonly Random random = new Random();
private static readonly NumberSource folderNumberSource = new NumberSource(0);
private readonly ILog log;
private readonly string rootFolder;
private readonly string folder;
private readonly List<List<TrackedFile>> fileSetStack = new List<List<TrackedFile>>();
public FileManager(ILog log, string rootFolder)
public const int ChunkSize = 1024 * 1024 * 100;
public FileManager(ILog log, string rootFolder, bool numberSubfolders = true)
{
folder = Path.Combine(rootFolder, folderNumberSource.GetNextNumber().ToString("D5"));
if (numberSubfolders)
{
folder = Path.Combine(rootFolder, folderNumberSource.GetNextNumber().ToString("D5"));
}
else
{
folder = rootFolder;
}
this.log = log;
this.rootFolder = rootFolder;
}
public TrackedFile CreateEmptyFile(string label = "")
@@ -41,10 +56,15 @@ namespace FileUtils
return result;
}
public TrackedFile GenerateFile(ByteSize size, string label)
public TrackedFile GenerateFile(ByteSize size, string label = "")
{
return GenerateFile(o => o.Random(size), label);
}
public TrackedFile GenerateFile(Action<IGenerateOption> options, string label = "")
{
var sw = Stopwatch.Begin(log);
var result = GenerateRandomFile(size, label);
var result = RunGenerators(options, label);
sw.End($"Generated file {result.Describe()}.");
return result;
}
@@ -57,16 +77,27 @@ namespace FileUtils
public void ScopedFiles(Action action)
{
PushFileSet();
action();
PopFileSet();
try
{
action();
}
finally
{
PopFileSet();
}
}
public T ScopedFiles<T>(Func<T> action)
{
PushFileSet();
var result = action();
PopFileSet();
return result;
try
{
return action();
}
finally
{
PopFileSet();
}
}
private void PushFileSet()
@@ -89,26 +120,35 @@ namespace FileUtils
if (!Directory.GetFiles(folder).Any()) DeleteDirectory();
}
private TrackedFile GenerateRandomFile(ByteSize size, string label)
private TrackedFile RunGenerators(Action<IGenerateOption> options, string label)
{
var result = CreateEmptyFile(label);
CheckSpaceAvailable(result, size);
var generators = GetGenerators(options);
CheckSpaceAvailable(result, generators.GetRequiredSpace());
GenerateFileBytes(result, size);
using var stream = new FileStream(result.Filename, FileMode.Append);
generators.Run(stream);
return result;
}
private void CheckSpaceAvailable(TrackedFile testFile, ByteSize size)
private GeneratorCollection GetGenerators(Action<IGenerateOption> options)
{
var result = new GeneratorCollection();
options(result);
return result;
}
private void CheckSpaceAvailable(TrackedFile testFile, long requiredSize)
{
var file = new FileInfo(testFile.Filename);
var drive = new DriveInfo(file.Directory!.Root.FullName);
var spaceAvailable = drive.TotalFreeSpace;
if (spaceAvailable < size.SizeInBytes)
if (spaceAvailable < requiredSize)
{
var msg = $"Not enough disk space. " +
$"{Formatter.FormatByteSize(size.SizeInBytes)} required. " +
$"{Formatter.FormatByteSize(requiredSize)} required. " +
$"{Formatter.FormatByteSize(spaceAvailable)} available.";
log.Log(msg);
@@ -116,34 +156,6 @@ namespace FileUtils
}
}
private void GenerateFileBytes(TrackedFile result, ByteSize size)
{
long bytesLeft = size.SizeInBytes;
int chunkSize = ChunkSize;
while (bytesLeft > 0)
{
try
{
var length = Math.Min(bytesLeft, chunkSize);
AppendRandomBytesToFile(result, length);
bytesLeft -= length;
}
catch
{
chunkSize = chunkSize / 2;
if (chunkSize < 1024) throw;
}
}
}
private void AppendRandomBytesToFile(TrackedFile result, long length)
{
var bytes = new byte[length];
random.NextBytes(bytes);
using var stream = new FileStream(result.Filename, FileMode.Append);
stream.Write(bytes, 0, bytes.Length);
}
private void EnsureDirectory()
{
Directory.CreateDirectory(folder);
+141
View File
@@ -0,0 +1,141 @@
using System.Text;
using Utils;
namespace FileUtils
{
public class GeneratorCollection : IGenerateOption
{
private readonly List<IGenerator> generators = new List<IGenerator>();
public IGenerateOption ByteRepeat(byte[] bytes, ByteSize size)
{
var times = size.SizeInBytes / bytes.Length;
generators.Add(new ByteRepeater(bytes, times));
return this;
}
public IGenerateOption ByteRepeat(byte[] bytes, int times)
{
generators.Add(new ByteRepeater(bytes, times));
return this;
}
public IGenerateOption Random(ByteSize size)
{
generators.Add(new RandomGenerator(size));
return this;
}
public IGenerateOption StringRepeat(string str, ByteSize size)
{
var times = size.SizeInBytes / str.Length;
generators.Add(new StringRepeater(str, times));
return this;
}
public IGenerateOption StringRepeat(string str, int times)
{
generators.Add(new StringRepeater(str, times));
return this;
}
public void Run(FileStream file)
{
foreach (var generator in generators)
{
generator.Generate(file);
}
}
public long GetRequiredSpace()
{
return generators.Sum(g => g.GetRequiredSpace());
}
}
public interface IGenerator
{
void Generate(FileStream file);
long GetRequiredSpace();
}
public class ByteRepeater : IGenerator
{
private readonly byte[] bytes;
private readonly long times;
public ByteRepeater(byte[] bytes, long times)
{
this.bytes = bytes;
this.times = times;
}
public void Generate(FileStream file)
{
for (var i = 0; i < times; i++)
{
file.Write(bytes, 0, bytes.Length);
}
}
public long GetRequiredSpace()
{
return bytes.Length * times;
}
}
public class StringRepeater : IGenerator
{
private readonly string str;
private readonly long times;
public StringRepeater(string str, long times)
{
this.str = str;
this.times = times;
}
public void Generate(FileStream file)
{
using var writer = new StreamWriter(file);
for (var i = 0; i < times; i++)
{
writer.Write(str);
}
}
public long GetRequiredSpace()
{
return Encoding.ASCII.GetBytes(str).Length * times;
}
}
public class RandomGenerator : IGenerator
{
private readonly Random random = new Random();
private readonly ByteSize size;
public RandomGenerator(ByteSize size)
{
this.size = size;
}
public void Generate(FileStream file)
{
var bytesLeft = size.SizeInBytes;
while (bytesLeft > 0)
{
var size = Math.Min(bytesLeft, FileManager.ChunkSize);
var bytes = new byte[size];
random.NextBytes(bytes);
file.Write(bytes, 0, bytes.Length);
bytesLeft -= size;
}
}
public long GetRequiredSpace()
{
return size.SizeInBytes;
}
}
}
+1 -1
View File
@@ -65,7 +65,7 @@ namespace FileUtils
if (readExpected == 0 && readActual == 0)
{
log.Log($"OK: '{Describe()}' is equal to '{actual.Describe()}'.");
log.Log($"OK: {Describe()} is equal to {actual.Describe()}.");
return;
}
@@ -1,41 +0,0 @@
using Utils;
namespace KubernetesWorkflow
{
public static class ByteSizeExtensions
{
public static string ToSuffixNotation(this ByteSize b)
{
long x = 1024;
var map = new Dictionary<long, string>
{
{ Pow(x, 4), "Ti" },
{ Pow(x, 3), "Gi" },
{ Pow(x, 2), "Mi" },
{ (x), "Ki" },
};
var bytes = b.SizeInBytes;
foreach (var pair in map)
{
if (bytes > pair.Key)
{
double bytesD = bytes;
double divD = pair.Key;
double numD = Math.Ceiling(bytesD / divD);
var v = Convert.ToInt64(numD);
return $"{v}{pair.Value}";
}
}
return $"{bytes}";
}
private static long Pow(long x, int v)
{
long result = 1;
for (var i = 0; i < v; i++) result *= x;
return result;
}
}
}
+9 -7
View File
@@ -11,7 +11,6 @@ namespace KubernetesWorkflow
private readonly string podName;
private readonly string recipeName;
private readonly string k8sNamespace;
private ILogHandler? logHandler;
private CancellationTokenSource cts;
private Task? worker;
private Exception? workerException;
@@ -27,11 +26,10 @@ namespace KubernetesWorkflow
cts = new CancellationTokenSource();
}
public void Start(ILogHandler logHandler)
public void Start()
{
if (worker != null) throw new InvalidOperationException();
this.logHandler = logHandler;
cts = new CancellationTokenSource();
worker = Task.Run(Worker);
}
@@ -50,7 +48,9 @@ namespace KubernetesWorkflow
public bool HasContainerCrashed()
{
using var client = new Kubernetes(config);
return HasContainerBeenRestarted(client);
var result = HasContainerBeenRestarted(client);
if (result) DownloadCrashedContainerLogs(client);
return result;
}
private void Worker()
@@ -83,14 +83,16 @@ namespace KubernetesWorkflow
private bool HasContainerBeenRestarted(Kubernetes client)
{
var podInfo = client.ReadNamespacedPod(podName, k8sNamespace);
return podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
var result = podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
if (result) log.Log("Pod crash detected for " + containerName);
return result;
}
private void DownloadCrashedContainerLogs(Kubernetes client)
{
log.Log("Pod crash detected for " + containerName);
using var stream = client.ReadNamespacedPodLog(podName, k8sNamespace, recipeName, previous: true);
logHandler!.Log(stream);
var handler = new WriteToFileLogHandler(log, "Crash detected for " + containerName);
handler.Log(stream);
}
}
}
@@ -1,12 +1,15 @@
using Logging;
namespace Core
namespace KubernetesWorkflow
{
public interface IDownloadedLog
{
void IterateLines(Action<string> action);
string ContainerName { get; }
void IterateLines(Action<string> action, params string[] thatContain);
string[] GetLinesContaining(string expectedString);
string[] FindLinesThatContain(params string[] tags);
string GetFilepath();
void DeleteFile();
}
@@ -14,12 +17,15 @@ namespace Core
{
private readonly LogFile logFile;
internal DownloadedLog(LogFile logFile)
internal DownloadedLog(WriteToFileLogHandler logHandler, string containerName)
{
this.logFile = logFile;
logFile = logHandler.LogFile;
ContainerName = containerName;
}
public void IterateLines(Action<string> action)
public string ContainerName { get; }
public void IterateLines(Action<string> action, params string[] thatContain)
{
using var file = File.OpenRead(logFile.FullFilename);
using var streamReader = new StreamReader(file);
@@ -27,7 +33,10 @@ namespace Core
var line = streamReader.ReadLine();
while (line != null)
{
action(line);
if (thatContain.All(line.Contains))
{
action(line);
}
line = streamReader.ReadLine();
}
}
@@ -71,6 +80,11 @@ namespace Core
return result.ToArray();
}
public string GetFilepath()
{
return logFile.FullFilename;
}
public void DeleteFile()
{
File.Delete(logFile.FullFilename);
@@ -16,6 +16,7 @@ namespace KubernetesWorkflow
{
var config = GetConfig();
UpdateHostAddress(config);
config.SkipTlsVerify = true; // Required for operation on Wings cluster.
return config;
}
+43 -11
View File
@@ -45,7 +45,7 @@ namespace KubernetesWorkflow
public void WaitUntilOnline(RunningContainer container)
{
WaitUntilDeploymentOnline(container.Recipe.Name);
WaitUntilDeploymentOnline(container);
}
public PodInfo GetPodInfo(RunningDeployment deployment)
@@ -64,14 +64,14 @@ namespace KubernetesWorkflow
if (waitTillStopped) WaitUntilPodsForDeploymentAreOffline(startResult.Deployment);
}
public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines)
public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines, bool? previous)
{
log.Debug();
var podName = GetPodName(container);
var recipeName = container.Recipe.Name;
using var stream = client.Run(c => c.ReadNamespacedPodLog(podName, K8sNamespace, recipeName, tailLines: tailLines));
using var stream = client.Run(c => c.ReadNamespacedPodLog(podName, K8sNamespace, recipeName, tailLines: tailLines, previous: previous));
logHandler.Log(stream);
}
@@ -115,7 +115,7 @@ namespace KubernetesWorkflow
});
}
public void DeleteAllNamespacesStartingWith(string prefix)
public void DeleteAllNamespacesStartingWith(string prefix, bool wait)
{
log.Debug();
@@ -124,25 +124,28 @@ namespace KubernetesWorkflow
foreach (var ns in namespaces)
{
DeleteNamespace(ns);
DeleteNamespace(ns, wait);
}
}
public void DeleteNamespace()
public void DeleteNamespace(bool wait)
{
log.Debug();
if (IsNamespaceOnline(K8sNamespace))
{
client.Run(c => c.DeleteNamespace(K8sNamespace, null, null, gracePeriodSeconds: 0));
if (wait) WaitUntilNamespaceDeleted(K8sNamespace);
}
}
public void DeleteNamespace(string ns)
public void DeleteNamespace(string ns, bool wait)
{
log.Debug();
if (IsNamespaceOnline(ns))
{
client.Run(c => c.DeleteNamespace(ns, null, null, gracePeriodSeconds: 0));
if (wait) WaitUntilNamespaceDeleted(ns);
}
}
@@ -532,7 +535,7 @@ namespace KubernetesWorkflow
}
if (set.Memory.SizeInBytes != 0)
{
result.Add("memory", new ResourceQuantity(set.Memory.ToSuffixNotation()));
result.Add("memory", new ResourceQuantity(set.Memory.SizeInBytes.ToString()));
}
return result;
}
@@ -712,7 +715,7 @@ namespace KubernetesWorkflow
{
return Time.Retry(() => GetPodForDeplomentInternal(deployment),
// We will wait up to 1 minute, k8s might be moving pods around.
maxRetries: 6,
maxTimeout: TimeSpan.FromMinutes(1),
retryTime: TimeSpan.FromSeconds(10),
description: "Find pod by label for deployment.");
}
@@ -871,15 +874,44 @@ namespace KubernetesWorkflow
WaitUntil(() => IsNamespaceOnline(K8sNamespace), nameof(WaitUntilNamespaceCreated));
}
private void WaitUntilDeploymentOnline(string deploymentName)
private void WaitUntilNamespaceDeleted(string @namespace)
{
WaitUntil(() => !IsNamespaceOnline(@namespace), nameof(WaitUntilNamespaceDeleted));
}
private void WaitUntilDeploymentOnline(RunningContainer container)
{
WaitUntil(() =>
{
var deployment = client.Run(c => c.ReadNamespacedDeployment(deploymentName, K8sNamespace));
CheckForCrash(container);
var deployment = client.Run(c => c.ReadNamespacedDeployment(container.Recipe.Name, K8sNamespace));
return deployment?.Status.AvailableReplicas != null && deployment.Status.AvailableReplicas > 0;
}, nameof(WaitUntilDeploymentOnline));
}
private void CheckForCrash(RunningContainer container)
{
var deploymentName = container.Recipe.Name;
var podName = GetPodName(container);
var podInfo = client.Run(c => c.ReadNamespacedPod(podName, K8sNamespace));
if (podInfo == null) return;
if (podInfo.Status == null) return;
if (podInfo.Status.ContainerStatuses == null) return;
var result = podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
if (result)
{
var msg = $"Pod crash detected for deployment {deploymentName} (pod:{podName})";
log.Error(msg);
DownloadPodLog(container, new WriteToFileLogHandler(log, msg), tailLines: null, previous: true);
throw new Exception(msg);
}
}
private void WaitUntilDeploymentOffline(string deploymentName)
{
WaitUntil(() =>
+24 -1
View File
@@ -1,4 +1,6 @@
namespace KubernetesWorkflow
using Logging;
namespace KubernetesWorkflow
{
public interface ILogHandler
{
@@ -20,4 +22,25 @@
protected abstract void ProcessLine(string line);
}
public class WriteToFileLogHandler : LogHandler, ILogHandler
{
public WriteToFileLogHandler(ILog sourceLog, string description)
{
LogFile = sourceLog.CreateSubfile();
var msg = $"{description} -->> {LogFile.FullFilename}";
sourceLog.Log(msg);
LogFile.Write(msg);
LogFile.WriteRaw(description);
}
public LogFile LogFile { get; }
protected override void ProcessLine(string line)
{
LogFile.WriteRaw(line);
}
}
}
@@ -1,4 +1,5 @@
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace KubernetesWorkflow.Recipe
{
@@ -21,14 +22,13 @@ namespace KubernetesWorkflow.Recipe
var typeName = GetTypeName(typeof(T));
var userData = Additionals.SingleOrDefault(a => a.Type == typeName);
if (userData == null) return default;
var jobject = (JObject)userData.UserData;
return jobject.ToObject<T>();
return JsonConvert.DeserializeObject<T>(userData.UserData);
}
private static Additional ConvertToAdditional(object userData)
{
var typeName = GetTypeName(userData.GetType());
return new Additional(typeName, userData);
return new Additional(typeName, JsonConvert.SerializeObject(userData));
}
private static string GetTypeName(Type type)
@@ -41,13 +41,13 @@ namespace KubernetesWorkflow.Recipe
public class Additional
{
public Additional(string type, object userData)
public Additional(string type, string userData)
{
Type = type;
UserData = userData;
}
public string Type { get; }
public object UserData { get; }
public string UserData { get; }
}
}
@@ -2,8 +2,9 @@
{
public class ContainerRecipe
{
public ContainerRecipe(int number, string? nameOverride, string image, ContainerResources resources, SchedulingAffinity schedulingAffinity, CommandOverride commandOverride, bool setCriticalPriority, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars, PodLabels podLabels, PodAnnotations podAnnotations, VolumeMount[] volumes, ContainerAdditionals additionals)
public ContainerRecipe(DateTime recipeCreatedUtc, int number, string? nameOverride, string image, ContainerResources resources, SchedulingAffinity schedulingAffinity, CommandOverride commandOverride, bool setCriticalPriority, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars, PodLabels podLabels, PodAnnotations podAnnotations, VolumeMount[] volumes, ContainerAdditionals additionals)
{
RecipeCreatedUtc = recipeCreatedUtc;
Number = number;
NameOverride = nameOverride;
Image = image;
@@ -31,6 +32,7 @@
if (exposedPorts.Any(p => string.IsNullOrEmpty(p.Tag))) throw new Exception("Port tags are required for all exposed ports.");
}
public DateTime RecipeCreatedUtc { get; }
public string Name { get; }
public int Number { get; }
public string? NameOverride { get; }
@@ -25,7 +25,7 @@ namespace KubernetesWorkflow.Recipe
Initialize(config);
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources, schedulingAffinity, commandOverride, setCriticalPriority,
var recipe = new ContainerRecipe(DateTime.UtcNow, containerNumber, config.NameOverride, Image, resources, schedulingAffinity, commandOverride, setCriticalPriority,
exposedPorts.ToArray(),
internalPorts.ToArray(),
envVars.ToArray(),
@@ -105,7 +105,7 @@ namespace KubernetesWorkflow.Recipe
protected void AddVolume(string name, string mountPath, string? subPath = null, string? secret = null, string? hostPath = null)
{
var size = 10.MB().ToSuffixNotation();
var size = 10.MB().SizeInBytes.ToString();
volumeMounts.Add(new VolumeMount(name, mountPath, subPath, size, secret, hostPath));
}
@@ -114,7 +114,7 @@ namespace KubernetesWorkflow.Recipe
volumeMounts.Add(new VolumeMount(
$"autovolume-{Guid.NewGuid().ToString().ToLowerInvariant()}",
mountPath,
resourceQuantity: volumeSize.ToSuffixNotation()));
resourceQuantity: volumeSize.SizeInBytes.ToString()));
}
protected void Additional(object userData)
+1 -4
View File
@@ -1,8 +1,5 @@
using k8s;
using k8s.Models;
using KubernetesWorkflow.Recipe;
using KubernetesWorkflow.Recipe;
using KubernetesWorkflow.Types;
using Newtonsoft.Json;
namespace KubernetesWorkflow
{
+35 -12
View File
@@ -15,10 +15,11 @@ namespace KubernetesWorkflow
PodInfo GetPodInfo(RunningPod pod);
CrashWatcher CreateCrashWatcher(RunningContainer container);
void Stop(RunningPod pod, bool waitTillStopped);
void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null);
void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null, bool? previous = null);
IDownloadedLog DownloadContainerLog(RunningContainer container, int? tailLines = null, bool? previous = null);
string ExecuteCommand(RunningContainer container, string command, params string[] args);
void DeleteNamespace();
void DeleteNamespacesStartingWith(string namespacePrefix);
void DeleteNamespace(bool wait);
void DeleteNamespacesStartingWith(string namespacePrefix, bool wait);
}
public class StartupWorkflow : IStartupWorkflow
@@ -60,7 +61,7 @@ namespace KubernetesWorkflow
var startResult = controller.BringOnline(recipes, location);
var containers = CreateContainers(startResult, recipes, startupConfig);
var rc = new RunningPod(startupConfig, startResult, containers);
var rc = new RunningPod(Guid.NewGuid().ToString(), startupConfig, startResult, containers);
cluster.Configuration.Hooks.OnContainersStarted(rc);
if (startResult.ExternalService != null)
@@ -99,21 +100,43 @@ namespace KubernetesWorkflow
public void Stop(RunningPod runningPod, bool waitTillStopped)
{
if (runningPod.IsStopped) return;
foreach (var c in runningPod.Containers)
{
c.StopLog = DownloadContainerLog(c);
}
runningPod.IsStopped = true;
K8s(controller =>
{
controller.Stop(runningPod.StartResult, waitTillStopped);
cluster.Configuration.Hooks.OnContainersStopped(runningPod);
});
cluster.Configuration.Hooks.OnContainersStopped(runningPod);
}
public void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null)
public void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null, bool? previous = null)
{
K8s(controller =>
{
controller.DownloadPodLog(container, logHandler, tailLines);
controller.DownloadPodLog(container, logHandler, tailLines, previous);
});
}
public IDownloadedLog DownloadContainerLog(RunningContainer container, int? tailLines = null, bool? previous = null)
{
var msg = $"Downloading container log for '{container.Name}'";
log.Log(msg);
var logHandler = new WriteToFileLogHandler(log, msg);
K8s(controller =>
{
controller.DownloadPodLog(container, logHandler, tailLines, previous);
});
return new DownloadedLog(logHandler, container.Name);
}
public string ExecuteCommand(RunningContainer container, string command, params string[] args)
{
return K8s(controller =>
@@ -122,19 +145,19 @@ namespace KubernetesWorkflow
});
}
public void DeleteNamespace()
public void DeleteNamespace(bool wait)
{
K8s(controller =>
{
controller.DeleteNamespace();
controller.DeleteNamespace(wait);
});
}
public void DeleteNamespacesStartingWith(string namespacePrefix)
public void DeleteNamespacesStartingWith(string namespacePrefix, bool wait)
{
K8s(controller =>
{
controller.DeleteAllNamespacesStartingWith(namespacePrefix);
controller.DeleteAllNamespacesStartingWith(namespacePrefix, wait);
});
}
@@ -147,7 +170,7 @@ namespace KubernetesWorkflow
var addresses = CreateContainerAddresses(startResult, r);
log.Debug($"{r}={name} -> container addresses: {string.Join(Environment.NewLine, addresses.Select(a => a.ToString()))}");
return new RunningContainer(name, r, addresses);
return new RunningContainer(log, Guid.NewGuid().ToString(), name, r, addresses);
}).ToArray();
}
@@ -7,21 +7,27 @@ namespace KubernetesWorkflow.Types
{
public class RunningContainer
{
public RunningContainer(string name, ContainerRecipe recipe, ContainerAddress[] addresses)
private readonly ILog log;
public RunningContainer(ILog log, string id, string name, ContainerRecipe recipe, ContainerAddress[] addresses)
{
this.log = log;
Id = id;
Name = name;
Recipe = recipe;
Addresses = addresses;
}
public string Id { get; }
public string Name { get; }
public ContainerRecipe Recipe { get; }
public ContainerAddress[] Addresses { get; }
public IDownloadedLog? StopLog { get; internal set; }
[JsonIgnore]
public RunningPod RunningPod { get; internal set; } = null!;
public Address GetAddress(ILog log, string portTag)
public Address GetAddress(string portTag)
{
var addresses = Addresses.Where(a => a.PortTag == portTag).ToArray();
if (!addresses.Any()) throw new Exception("No addresses found for portTag: " + portTag);
@@ -50,5 +56,21 @@ namespace KubernetesWorkflow.Types
}
throw new Exception("Running location not known.");
}
public override string ToString()
{
return Name;
}
public override bool Equals(object? obj)
{
return obj is RunningContainer container &&
Id == container.Id;
}
public override int GetHashCode()
{
return HashCode.Combine(Id);
}
}
}
@@ -4,8 +4,9 @@ namespace KubernetesWorkflow.Types
{
public class RunningPod
{
public RunningPod(StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
public RunningPod(string id, StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
{
Id = id;
StartupConfig = startupConfig;
StartResult = startResult;
Containers = containers;
@@ -13,6 +14,7 @@ namespace KubernetesWorkflow.Types
foreach (var c in containers) c.RunningPod = this;
}
public string Id { get; }
public StartupConfig StartupConfig { get; }
public StartResult StartResult { get; }
public RunningContainer[] Containers { get; }
@@ -20,13 +22,33 @@ namespace KubernetesWorkflow.Types
[JsonIgnore]
public string Name
{
get { return $"{Containers.Length}x '{Containers.First().Name}'"; }
get { return $"'{string.Join("&", Containers.Select(c => c.Name).ToArray())}'"; }
}
[JsonIgnore]
public bool IsStopped { get; internal set; }
public string Describe()
{
return string.Join(",", Containers.Select(c => c.Name));
}
public override bool Equals(object? obj)
{
return obj is RunningPod pod &&
Id == pod.Id;
}
public override int GetHashCode()
{
return HashCode.Combine(Id);
}
public override string ToString()
{
if (IsStopped) return Name + " (*)";
return Name;
}
}
public static class RunningContainersExtensions
+1 -1
View File
@@ -77,7 +77,7 @@ namespace Logging
return new LogFile($"{GetFullName()}_{GetSubfileNumber()}", ext);
}
private string ApplyReplacements(string str)
protected string ApplyReplacements(string str)
{
if (IsDebug) return str;
foreach (var replacement in replacements)
+1 -1
View File
@@ -9,7 +9,7 @@
public override void Log(string message)
{
Console.WriteLine(message);
Console.WriteLine(ApplyReplacements(message));
}
}
}
@@ -20,6 +20,7 @@ namespace NethereumWorkflow.BlockUtils
public BlockTimeEntry Get(ulong blockNumber)
{
bounds.Initialize();
var b = cache.Get(blockNumber);
if (b != null) return b;
return GetBlock(blockNumber);
@@ -45,7 +46,7 @@ namespace NethereumWorkflow.BlockUtils
private ulong Log(Func<ulong> operation)
{
var sw = Stopwatch.Begin(log, nameof(BlockTimeFinder));
var sw = Stopwatch.Begin(log, nameof(BlockTimeFinder), true);
var result = operation();
sw.End($"(Bounds: [{bounds.Genesis.BlockNumber}-{bounds.Current.BlockNumber}] Cache: {cache.Size})");
@@ -117,6 +117,7 @@ namespace NethereumWorkflow
}
return new BlockInterval(
timeRange: timeRange,
from: fromBlock.Value,
to: toBlock.Value
);
@@ -0,0 +1,61 @@
namespace OverwatchTranscript
{
public class ActionQueue
{
// Using ConcurrentQueue<> here would make this process slower.
private readonly object queueLock = new object();
private readonly AutoResetEvent signal = new AutoResetEvent(false);
private List<Action> queue = new List<Action>();
private Task queueWorker = null!;
private bool stopping = false;
public void Start()
{
queueWorker = Task.Run(QueueWorker);
}
public int Count { get; private set; }
public void StopAndJoin()
{
stopping = true;
queueWorker.Wait();
if (queue.Count > 0) throw new Exception("not all acions handled");
queueWorker.Dispose();
}
public void Add(Action action)
{
if (stopping) throw new Exception("queue stopping");
lock (queueLock)
{
queue.Add(action);
Count = queue.Count;
}
signal.Set();
}
private void QueueWorker()
{
while (true)
{
signal.WaitOne(10);
List<Action> work = null!;
lock (queueLock)
{
work = queue;
queue = new List<Action>();
Count = 0;
}
if (stopping && !work.Any()) return;
foreach (var action in work)
{
action();
}
}
}
}
}
+110
View File
@@ -0,0 +1,110 @@
using Logging;
using System.Collections.Concurrent;
namespace OverwatchTranscript
{
public class BucketSet
{
private const int numberOfActiveBuckets = 10;
private readonly ILog log;
private readonly string workingDir;
private readonly object _bucketLock = new object();
private readonly List<EventBucketWriter> fullBuckets = new List<EventBucketWriter>();
private readonly List<EventBucketWriter> activeBuckets = new List<EventBucketWriter>();
private readonly ActionQueue queue = new ActionQueue();
private int activeBucketIndex = 0;
private bool closed = false;
private string internalErrors = string.Empty;
public BucketSet(ILog log, string workingDir)
{
this.log = log;
this.workingDir = workingDir;
for (var i = 0; i < numberOfActiveBuckets;i++)
{
AddNewBucket();
}
queue.Start();
}
public void Add(DateTime utc, object payload)
{
if (closed) throw new Exception("Buckets already closed!");
queue.Add(() => AddInternal(utc, payload));
if (queue.Count > 1000)
{
Thread.Sleep(1);
}
}
public IFinalizedBucket[] FinalizeBuckets()
{
closed = true;
queue.StopAndJoin();
if (IsEmpty()) throw new Exception("No entries have been added.");
if (!string.IsNullOrEmpty(internalErrors)) throw new Exception(internalErrors);
var buckets = fullBuckets.Concat(activeBuckets).ToArray();
log.Debug($"Finalizing {buckets.Length} buckets...");
var finalized = new ConcurrentBag<IFinalizedBucket>();
var tasks = Parallel.ForEach(buckets, b => finalized.Add(b.FinalizeBucket()));
if (!tasks.IsCompleted) throw new Exception("Failed to finalize buckets: " + tasks);
return finalized.ToArray();
}
private bool IsEmpty()
{
return fullBuckets.All(b => b.Count == 0) && activeBuckets.All(b => b.Count == 0);
}
private void AddInternal(DateTime utc, object payload)
{
try
{
lock (_bucketLock)
{
var current = activeBuckets[activeBucketIndex];
current.Add(utc, payload);
activeBucketIndex = (activeBucketIndex + 1) % numberOfActiveBuckets;
if (current.IsFull)
{
log.Debug("Bucket is full. New bucket...");
fullBuckets.Add(current);
activeBuckets.Remove(current);
AddNewBucket();
}
}
}
catch (Exception ex)
{
internalErrors += ex.ToString();
log.Error(ex.ToString());
}
}
private static int bucketSizeIndex = 0;
private static int[] bucketSizes = new[]
{
10000,
15000,
20000,
};
private void AddNewBucket()
{
lock (_bucketLock)
{
var size = bucketSizes[bucketSizeIndex];
bucketSizeIndex = (bucketSizeIndex + 1) % bucketSizes.Length;
activeBuckets.Add(new EventBucketWriter(log, Path.Combine(workingDir, Guid.NewGuid().ToString()), size));
}
}
}
}
@@ -0,0 +1,143 @@
using Logging;
using Newtonsoft.Json;
using System.Collections.Concurrent;
namespace OverwatchTranscript
{
public interface IFinalizedBucket
{
bool IsEmpty { get; }
DateTime? SeeTopUtc();
BucketTop? TakeTop();
}
public class BucketTop
{
public BucketTop(DateTime utc, OverwatchEvent[] events)
{
Utc = utc;
Events = events;
}
public DateTime Utc { get; }
public OverwatchEvent[] Events { get; }
}
public class EventBucketReader : IFinalizedBucket
{
private readonly string bucketFile;
private readonly ConcurrentQueue<BucketTop> topQueue = new ConcurrentQueue<BucketTop>();
private readonly AutoResetEvent itemDequeued = new AutoResetEvent(false);
private bool stopping;
public EventBucketReader(ILog log, string bucketFile)
{
this.bucketFile = bucketFile;
if (!File.Exists(bucketFile)) throw new Exception("Doesn't exist: " + bucketFile);
log.Debug("Read Bucket open: " + bucketFile);
Task.Run(ReadBucket);
}
public bool IsEmpty { get; private set; }
public DateTime? SeeTopUtc()
{
if (IsEmpty) return null;
while (true)
{
UpdateIsEmpty();
if (IsEmpty) return null;
if (topQueue.TryPeek(out BucketTop? top))
{
return top.Utc;
}
}
}
public BucketTop? TakeTop()
{
if (IsEmpty) return null;
while (true)
{
UpdateIsEmpty();
if (IsEmpty) return null;
if (topQueue.TryDequeue(out BucketTop? top))
{
itemDequeued.Set();
return top;
}
}
}
private void ReadBucket()
{
using var file = File.OpenRead(bucketFile);
using var reader = new StreamReader(file);
while (true)
{
while (topQueue.Count < 5)
{
var top = CreateNewTop(reader);
if (top != null)
{
topQueue.Enqueue(top);
}
else
{
stopping = true;
return;
}
}
itemDequeued.Reset();
itemDequeued.WaitOne();
}
}
private void UpdateIsEmpty()
{
var empty = stopping && topQueue.IsEmpty;
if (!IsEmpty && empty)
{
File.Delete(bucketFile);
IsEmpty = true;
}
}
private EventBucketEntry? nextEntry = null;
private BucketTop? CreateNewTop(StreamReader reader)
{
if (nextEntry == null)
{
nextEntry = ReadEntry(reader);
if (nextEntry == null) return null;
}
var topEntry = nextEntry;
var entries = new List<EventBucketEntry>
{
topEntry
};
nextEntry = ReadEntry(reader);
while (nextEntry != null && nextEntry.Utc == topEntry.Utc)
{
entries.Add(nextEntry);
nextEntry = ReadEntry(reader);
}
return new BucketTop(topEntry.Utc, entries.Select(e => e.Event).ToArray());
}
private EventBucketEntry? ReadEntry(StreamReader reader)
{
var line = reader.ReadLine();
if (string.IsNullOrEmpty(line)) return null;
return JsonConvert.DeserializeObject<EventBucketEntry>(line);
}
}
}
@@ -0,0 +1,113 @@
using Logging;
using Newtonsoft.Json;
namespace OverwatchTranscript
{
public class EventBucketWriter
{
private const int MaxBuffer = 1000;
private readonly object _lock = new object();
private bool closed = false;
private readonly ILog log;
private readonly string bucketFile;
private readonly int maxCount;
private readonly List<EventBucketEntry> buffer = new List<EventBucketEntry>();
public EventBucketWriter(ILog log, string bucketFile, int maxCount)
{
this.log = log;
this.bucketFile = bucketFile;
this.maxCount = maxCount;
if (File.Exists(bucketFile)) throw new Exception("Already exists");
log.Debug("Write Bucket open: " + bucketFile);
}
public int Count { get; private set; }
public bool IsFull { get; private set; }
public void Add(DateTime utc, object payload)
{
lock (_lock)
{
if (closed) throw new Exception("Already closed");
AddToBuffer(utc, payload);
BufferToFile(emptyBuffer: false);
}
}
public IFinalizedBucket FinalizeBucket()
{
lock (_lock)
{
closed = true;
BufferToFile(emptyBuffer: true);
SortFileByTimestamps();
}
log.Debug($"Finalized bucket with {Count} entries");
return new EventBucketReader(log, bucketFile);
}
public override string ToString()
{
return $"EventBucket: " + Count;
}
private void AddToBuffer(DateTime utc, object payload)
{
var typeName = payload.GetType().FullName;
if (string.IsNullOrEmpty(typeName)) throw new Exception("Empty typename for payload");
if (utc == default) throw new Exception("DateTimeUtc not set");
var entry = new EventBucketEntry
{
Utc = utc,
Event = new OverwatchEvent
{
Type = typeName,
Payload = Json.Serialize(payload)
}
};
Count++;
IsFull = Count > maxCount;
buffer.Add(entry);
}
private void BufferToFile(bool emptyBuffer)
{
if (emptyBuffer || buffer.Count > MaxBuffer)
{
using var file = File.Open(bucketFile, FileMode.Append);
using var writer = new StreamWriter(file);
foreach (var entry in buffer)
{
writer.WriteLine(Json.Serialize(entry));
}
log.Debug($"Bucket wrote {buffer.Count} entries to file.");
buffer.Clear();
}
}
private void SortFileByTimestamps()
{
var lines = File.ReadAllLines(bucketFile);
var entries = lines.Select(Json.Deserialize<EventBucketEntry>)
.Cast<EventBucketEntry>()
.OrderBy(e => e.Utc)
.ToArray();
File.Delete(bucketFile);
File.WriteAllLines(bucketFile, entries.Select(e => Json.Serialize(e)));
}
}
[Serializable]
public class EventBucketEntry
{
public DateTime Utc { get; set; }
public OverwatchEvent Event { get; set; } = new();
}
}
+27
View File
@@ -0,0 +1,27 @@
using Newtonsoft.Json;
using System.Globalization;
namespace OverwatchTranscript
{
public static class Json
{
private static JsonSerializerSettings settings = new JsonSerializerSettings
{
Formatting = Formatting.None,
NullValueHandling = NullValueHandling.Ignore,
Culture = CultureInfo.InvariantCulture,
DateFormatHandling = DateFormatHandling.IsoDateFormat,
FloatFormatHandling = FloatFormatHandling.Symbol
};
public static string Serialize(object obj, Formatting formatting = Formatting.None)
{
return JsonConvert.SerializeObject(obj, formatting, settings);
}
public static T Deserialize<T>(string json)
{
return JsonConvert.DeserializeObject<T>(json)!;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
namespace OverwatchTranscript
{
[Serializable]
public class OverwatchTranscript
{
public OverwatchHeader Header { get; set; } = new();
public OverwatchMomentReference[] MomentReferences { get; set; } = Array.Empty<OverwatchMomentReference>();
}
[Serializable]
public class OverwatchMomentReference
{
public string MomentsFile { get; set; } = string.Empty;
public int NumberOfMoments { get; set; }
public int NumberOfEvents { get; set; }
public DateTime EarliestUtc { get; set; }
public DateTime LatestUtc { get; set; }
}
[Serializable]
public class OverwatchHeader
{
public OverwatchCommonHeader Common { get; set; } = new();
public OverwatchHeaderEntry[] Entries { get; set; } = Array.Empty<OverwatchHeaderEntry>();
}
[Serializable]
public class OverwatchCommonHeader
{
public long NumberOfMoments { get; set; }
public long NumberOfEvents { get; set; }
public DateTime EarliestUtc { get; set; }
public DateTime LatestUtc { get; set; }
}
[Serializable]
public class OverwatchHeaderEntry
{
public string Key { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
}
[Serializable]
public class OverwatchMoment
{
public DateTime Utc { get; set; }
public OverwatchEvent[] Events { get; set; } = Array.Empty<OverwatchEvent>();
}
[Serializable]
public class OverwatchEvent
{
public string Type { get; set; } = string.Empty;
public string Payload { get; set; } = string.Empty;
}
}
@@ -0,0 +1,104 @@
using Newtonsoft.Json;
namespace OverwatchTranscript
{
public class MomentReader
{
private readonly OverwatchTranscript model;
private readonly string workingDir;
private int referenceIndex = 0;
private int momentsRead = 0;
private OpenReference currentRef;
public MomentReader(OverwatchTranscript model, string workingDir)
{
this.model = model;
this.workingDir = workingDir;
currentRef = CreateOpenReference();
}
public OverwatchMoment? Next()
{
if (referenceIndex >= model.MomentReferences.Length) return null;
var moment = currentRef.ReadNext();
if (moment == null)
{
Close();
// This reference file ran out.
// The number of moments read should match exactly the number of moments
// describe in the reference. If not, error:
var expected = model.MomentReferences[referenceIndex].NumberOfMoments;
if (momentsRead != expected)
{
throw new Exception("Number of moments read from referenced file does not match number of moments value in model. " +
$"Reads: { momentsRead} - model.MomentReferences[{referenceIndex}].NumberOfMoment: {expected}");
}
referenceIndex++;
if (referenceIndex < model.MomentReferences.Length)
{
// Proceed to next reference file.
currentRef = CreateOpenReference();
momentsRead = 0;
return Next();
}
else
{
// That was the last one.
return null;
}
}
else
{
momentsRead++;
return moment;
}
}
public void Close()
{
if (currentRef != null)
{
currentRef.Close();
currentRef = null!;
}
}
private OpenReference CreateOpenReference()
{
var filepath = Path.Combine(workingDir, model.MomentReferences[referenceIndex].MomentsFile);
return new OpenReference(filepath);
}
private class OpenReference
{
private readonly FileStream file;
private readonly StreamReader reader;
public OpenReference(string filePath)
{
file = File.OpenRead(filePath);
reader = new StreamReader(file);
}
public OverwatchMoment? ReadNext()
{
var line = reader.ReadLine();
if (string.IsNullOrEmpty(line)) return null;
return JsonConvert.DeserializeObject<OverwatchMoment>(line);
}
public void Close()
{
reader.Close();
file.Close();
reader.Dispose();
file.Dispose();
}
}
}
}
@@ -0,0 +1,151 @@
using Logging;
using Newtonsoft.Json;
namespace OverwatchTranscript
{
public class MomentReferenceBuilder
{
private const int MaxMomentsPerReference = 10000;
private readonly ILog log;
private readonly string workingDir;
public MomentReferenceBuilder(ILog log, string workingDir)
{
this.log = log;
this.workingDir = workingDir;
}
public OverwatchMomentReference[] Build(IFinalizedBucket[] finalizedBuckets)
{
var result = new List<OverwatchMomentReference>();
var currentBuilder = new Builder(log, workingDir);
var buckets = finalizedBuckets.ToList();
log.Debug($"Building references for {buckets.Count} buckets.");
while (buckets.Any())
{
buckets.RemoveAll(b => b.IsEmpty);
if (!buckets.Any()) break;
var earliestUtc = GetEarliestUtc(buckets);
if (earliestUtc == null) continue;
var tops = CollectAllTopsForUtc(earliestUtc.Value, buckets);
var moment = ConvertTopsToMoment(tops);
currentBuilder.Add(moment);
if (currentBuilder.NumberOfMoments == MaxMomentsPerReference)
{
result.Add(currentBuilder.Build());
currentBuilder = new Builder(log, workingDir);
}
}
if (currentBuilder.NumberOfMoments > 0)
{
result.Add(currentBuilder.Build());
}
return result.ToArray();
}
private OverwatchMoment ConvertTopsToMoment(List<BucketTop> tops)
{
var discintUtc = tops.Select(e => e.Utc).Distinct().ToArray();
if (discintUtc.Length != 1) throw new Exception("UTC mixing in moment construction.");
return new OverwatchMoment
{
Utc = tops[0].Utc,
Events = tops.SelectMany(e => e.Events).ToArray()
};
}
private List<BucketTop> CollectAllTopsForUtc(DateTime earliestUtc, List<IFinalizedBucket> buckets)
{
var result = new List<BucketTop>();
foreach (var bucket in buckets)
{
if (bucket.IsEmpty) continue;
var utc = bucket.SeeTopUtc();
if (utc == null) continue;
if (utc.Value == earliestUtc)
{
var top = bucket.TakeTop();
if (top == null) throw new Exception("top was null after top utc was not");
result.Add(top);
}
}
return result;
}
private DateTime? GetEarliestUtc(List<IFinalizedBucket> buckets)
{
var earliest = DateTime.MaxValue;
foreach (var bucket in buckets)
{
var utc = bucket.SeeTopUtc();
if (utc == null) return null;
if (utc.Value < earliest) earliest = utc.Value;
}
return earliest;
}
public class Builder
{
private readonly ILog log;
private readonly string workingDir;
private OverwatchMomentReference reference;
private readonly ActionQueue queue = new ActionQueue();
public Builder(ILog log, string workingDir)
{
reference = new OverwatchMomentReference
{
MomentsFile = Guid.NewGuid().ToString(),
EarliestUtc = DateTime.MaxValue,
LatestUtc = DateTime.MinValue,
NumberOfEvents = 0,
NumberOfMoments = 0,
};
this.log = log;
this.workingDir = workingDir;
queue.Start();
}
public int NumberOfMoments => reference.NumberOfMoments;
public void Add(OverwatchMoment moment)
{
if (moment.Utc < reference.EarliestUtc) reference.EarliestUtc = moment.Utc;
if (moment.Utc > reference.LatestUtc) reference.LatestUtc = moment.Utc;
reference.NumberOfMoments++;
reference.NumberOfEvents += moment.Events.Length;
var filePath = Path.Combine(workingDir, reference.MomentsFile);
queue.Add(() =>
{
File.AppendAllLines(filePath, new[]
{
Json.Serialize(moment)
});
});
}
public OverwatchMomentReference Build()
{
queue.StopAndJoin();
log.Debug($"Created reference with {reference.NumberOfMoments} moments and {reference.NumberOfEvents} events...");
var result = reference;
reference = null!;
return result;
}
}
}
}
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Logging\Logging.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,23 @@
using Logging;
namespace OverwatchTranscript
{
public static class Transcript
{
public static ITranscriptWriter NewWriter(ILog log)
{
log = new LogPrefixer(log, "(TranscriptWriter) ");
return new TranscriptWriter(log, NewWorkDir());
}
public static ITranscriptReader NewReader(string transcriptFile)
{
return new TranscriptReader(NewWorkDir(), transcriptFile);
}
private static string NewWorkDir()
{
return Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
}
}
}
@@ -0,0 +1,8 @@
namespace OverwatchTranscript
{
public static class TranscriptConstants
{
public const string TranscriptFilename = "transcript.json";
public const string ArtifactFolderName = "artifacts";
}
}
@@ -0,0 +1,264 @@
using Newtonsoft.Json;
using System.IO;
using System;
using System.IO.Compression;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
namespace OverwatchTranscript
{
public interface ITranscriptReader
{
OverwatchCommonHeader Header { get; }
T GetHeader<T>(string key);
void AddMomentHandler(Action<ActivateMoment> handler);
void AddEventHandler<T>(Action<ActivateEvent<T>> handler);
bool Next();
void Close();
}
public class TranscriptReader : ITranscriptReader
{
private readonly object handlersLock = new object();
private readonly string transcriptFile;
private readonly string artifactsFolder;
private readonly List<Action<ActivateMoment>> momentHandlers = new List<Action<ActivateMoment>>();
private readonly Dictionary<string, List<Action<ActivateMoment, string>>> eventHandlers = new Dictionary<string, List<Action<ActivateMoment, string>>>();
private readonly string workingDir;
private readonly OverwatchTranscript model;
private bool closed;
private long momentCounter;
private readonly ConcurrentQueue<OverwatchMoment> queue = new ConcurrentQueue<OverwatchMoment>();
private readonly Task queueFiller;
public TranscriptReader(string workingDir, string inputFilename)
{
closed = false;
this.workingDir = workingDir;
transcriptFile = Path.Combine(workingDir, TranscriptConstants.TranscriptFilename);
artifactsFolder = Path.Combine(workingDir, TranscriptConstants.ArtifactFolderName);
if (!Directory.Exists(workingDir)) Directory.CreateDirectory(workingDir);
if (File.Exists(transcriptFile) || Directory.Exists(artifactsFolder)) throw new Exception("workingdir not clean");
model = LoadModel(inputFilename);
queueFiller = Task.Run(() => FillQueue(model, workingDir));
}
public OverwatchCommonHeader Header
{
get
{
CheckClosed();
return model.Header.Common;
}
}
public T GetHeader<T>(string key)
{
CheckClosed();
var value = model.Header.Entries.First(e => e.Key == key).Value;
return JsonConvert.DeserializeObject<T>(value)!;
}
public void AddMomentHandler(Action<ActivateMoment> handler)
{
CheckClosed();
lock (handlersLock)
{
momentHandlers.Add(handler);
}
}
public void AddEventHandler<T>(Action<ActivateEvent<T>> handler)
{
CheckClosed();
var typeName = typeof(T).FullName;
if (string.IsNullOrEmpty(typeName)) throw new Exception("Empty typename for payload");
lock (handlersLock)
{
if (eventHandlers.ContainsKey(typeName))
{
eventHandlers[typeName].Add(CreateEventAction(handler));
}
else
{
eventHandlers.Add(typeName, new List<Action<ActivateMoment, string>>
{
CreateEventAction(handler)
});
}
}
}
private readonly object nextLock = new object();
private OverwatchMoment? moment = null;
private OverwatchMoment? next = null;
public bool Next()
{
CheckClosed();
OverwatchMoment? m = null;
TimeSpan? duration = null;
lock (nextLock)
{
if (next == null)
{
if (!queue.TryDequeue(out moment)) return false;
queue.TryDequeue(out next);
}
else
{
moment = next;
next = null;
queue.TryDequeue(out next);
}
m = moment;
duration = GetMomentDuration();
}
ActivateMoment(moment, duration);
return true;
}
public void Close()
{
CheckClosed();
closed = true;
queueFiller.Wait();
Directory.Delete(workingDir, true);
}
private Action<ActivateMoment, string> CreateEventAction<T>(Action<ActivateEvent<T>> handler)
{
return (m, s) =>
{
handler(new ActivateEvent<T>(m, JsonConvert.DeserializeObject<T>(s)!));
};
}
private void FillQueue(OverwatchTranscript model, string workingDir)
{
var reader = new MomentReader(model, workingDir);
while (true)
{
if (closed)
{
reader.Close();
return;
}
while (queue.Count < 10)
{
var moment = reader.Next();
if (moment == null)
{
reader.Close();
return;
}
queue.Enqueue(moment);
}
Thread.Sleep(1);
}
}
private TimeSpan? GetMomentDuration()
{
if (moment == null) return null;
if (next == null) return null;
return next.Utc - moment.Utc;
}
private void ActivateMoment(OverwatchMoment moment, TimeSpan? duration)
{
var m = new ActivateMoment(moment.Utc, duration, momentCounter);
lock (handlersLock)
{
ActivateMomentHandlers(m);
foreach (var @event in moment.Events)
{
ActivateEventHandlers(m, @event);
}
}
momentCounter++;
}
private void ActivateMomentHandlers(ActivateMoment m)
{
foreach (var handler in momentHandlers)
{
handler(m);
}
}
private void ActivateEventHandlers(ActivateMoment m, OverwatchEvent @event)
{
if (!eventHandlers.ContainsKey(@event.Type)) return;
var handlers = eventHandlers[@event.Type];
foreach (var handler in handlers)
{
handler(m, @event.Payload);
}
}
private OverwatchTranscript LoadModel(string inputFilename)
{
ZipFile.ExtractToDirectory(inputFilename, workingDir);
if (!File.Exists(transcriptFile))
{
closed = true;
throw new Exception("Is not a transcript file. Unzipped to: " + workingDir);
}
return JsonConvert.DeserializeObject<OverwatchTranscript>(File.ReadAllText(transcriptFile))!;
}
private void CheckClosed()
{
if (closed) throw new Exception("Transcript has already been closed.");
}
}
public class ActivateMoment
{
public ActivateMoment(DateTime utc, TimeSpan? duration, long index)
{
Utc = utc;
Duration = duration;
Index = index;
}
public DateTime Utc { get; }
public TimeSpan? Duration { get; }
public long Index { get; }
}
public class ActivateEvent<T>
{
public ActivateEvent(ActivateMoment moment, T payload)
{
Moment = moment;
Payload = payload;
}
public ActivateMoment Moment { get; }
public T Payload { get; }
}
}
@@ -0,0 +1,130 @@
using Logging;
using Newtonsoft.Json;
using System.IO.Compression;
namespace OverwatchTranscript
{
public interface ITranscriptWriter
{
void AddHeader(string key, object value);
void Add(DateTime utc, object payload);
void IncludeArtifact(string filePath);
void Write(string outputFilename);
}
public class TranscriptWriter : ITranscriptWriter
{
private readonly object _lock = new object();
private readonly MomentReferenceBuilder builder;
private readonly string transcriptFile;
private readonly string artifactsFolder;
private readonly Dictionary<string, string> header = new Dictionary<string, string>();
private readonly BucketSet bucketSet;
private readonly ILog log;
private readonly string workingDir;
private bool closed;
public TranscriptWriter(ILog log, string workingDir)
{
closed = false;
this.log = log;
this.workingDir = workingDir;
bucketSet = new BucketSet(log, workingDir);
builder = new MomentReferenceBuilder(log, workingDir);
transcriptFile = Path.Combine(workingDir, TranscriptConstants.TranscriptFilename);
artifactsFolder = Path.Combine(workingDir, TranscriptConstants.ArtifactFolderName);
if (!Directory.Exists(workingDir)) Directory.CreateDirectory(workingDir);
if (File.Exists(transcriptFile) || Directory.Exists(artifactsFolder)) throw new Exception("workingdir not clean");
}
public void Add(DateTime utc, object payload)
{
CheckClosed();
bucketSet.Add(utc, payload);
}
public void AddHeader(string key, object value)
{
CheckClosed();
lock (_lock)
{
header.Add(key, Json.Serialize(value));
}
}
public void IncludeArtifact(string filePath)
{
CheckClosed();
if (!File.Exists(filePath)) throw new Exception("File not found: " + filePath);
if (!Directory.Exists(artifactsFolder)) Directory.CreateDirectory(artifactsFolder);
var name = Path.GetFileName(filePath);
File.Copy(filePath, Path.Combine(artifactsFolder, name), overwrite: false);
}
public void Write(string outputFilename)
{
CheckClosed();
closed = true;
var momentReferences = builder.Build(bucketSet.FinalizeBuckets());
var model = CreateModel(momentReferences);
File.WriteAllText(transcriptFile, Json.Serialize(model, Formatting.Indented));
ZipFile.CreateFromDirectory(workingDir, outputFilename);
log.Debug($"Transcript written to {outputFilename}");
log.Debug($"Common header: {Json.Serialize(model.Header.Common, Formatting.Indented)}");
Directory.Delete(workingDir, true);
log.Debug($"Workdir {workingDir} deleted");
}
private OverwatchTranscript CreateModel(OverwatchMomentReference[] momentReferences)
{
lock (_lock)
{
var model = new OverwatchTranscript
{
Header = new OverwatchHeader
{
Common = CreateCommonHeader(momentReferences),
Entries = header.Select(h =>
{
return new OverwatchHeaderEntry
{
Key = h.Key,
Value = h.Value
};
}).ToArray()
},
MomentReferences = momentReferences
};
header.Clear();
return model;
}
}
private OverwatchCommonHeader CreateCommonHeader(OverwatchMomentReference[] momentReferences)
{
var moments = momentReferences.Sum(m => m.NumberOfMoments);
var events = momentReferences.Sum(m => m.NumberOfEvents);
var earliest = momentReferences.Min(m => m.EarliestUtc);
var latest = momentReferences.Max(m => m.LatestUtc);
return new OverwatchCommonHeader
{
NumberOfMoments = moments,
NumberOfEvents = events,
EarliestUtc = earliest,
LatestUtc = latest
};
}
private void CheckClosed()
{
if (closed) throw new Exception("Transcript has already been written. Cannot modify or write again.");
}
}
}
+4 -1
View File
@@ -2,7 +2,7 @@
{
public class BlockInterval
{
public BlockInterval(ulong from, ulong to)
public BlockInterval(TimeRange timeRange, ulong from, ulong to)
{
if (from < to)
{
@@ -14,10 +14,13 @@
From = to;
To = from;
}
TimeRange = timeRange;
}
public ulong From { get; }
public ulong To { get; }
public TimeRange TimeRange { get; }
public ulong NumberOfBlocks => To - From;
public override string ToString()
{
-1
View File
@@ -13,7 +13,6 @@
public long SizeInBytes { get; }
public long ToMB()
{
return SizeInBytes / (1024 * 1024);
+11
View File
@@ -11,5 +11,16 @@
remainingItems.RemoveAt(i);
return result;
}
public static T[] Shuffled<T>(T[] items)
{
var result = new List<T>();
var source = items.ToList();
while (source.Any())
{
result.Add(RandomUtils.PickOneRandom(source));
}
return result.ToArray();
}
}
}
+131
View File
@@ -0,0 +1,131 @@
namespace Utils
{
public class Retry
{
private readonly string description;
private readonly TimeSpan maxTimeout;
private readonly TimeSpan sleepAfterFail;
private readonly Action<Failure> onFail;
public Retry(string description, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action<Failure> onFail)
{
this.description = description;
this.maxTimeout = maxTimeout;
this.sleepAfterFail = sleepAfterFail;
this.onFail = onFail;
}
public void Run(Action task)
{
var run = new RetryRun(description, task, maxTimeout, sleepAfterFail, onFail);
run.Run();
}
public T Run<T>(Func<T> task)
{
T? result = default;
var run = new RetryRun(description, () =>
{
result = task();
}, maxTimeout, sleepAfterFail, onFail);
run.Run();
return result!;
}
private class RetryRun
{
private readonly string description;
private readonly Action task;
private readonly TimeSpan maxTimeout;
private readonly TimeSpan sleepAfterFail;
private readonly Action<Failure> onFail;
private readonly DateTime start = DateTime.UtcNow;
private readonly List<Failure> failures = new List<Failure>();
private int tryNumber;
private DateTime tryStart;
public RetryRun(string description, Action task, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action<Failure> onFail)
{
this.description = description;
this.task = task;
this.maxTimeout = maxTimeout;
this.sleepAfterFail = sleepAfterFail;
this.onFail = onFail;
tryNumber = 0;
tryStart = DateTime.UtcNow;
}
public void Run()
{
while (true)
{
CheckMaximums();
tryNumber++;
tryStart = DateTime.UtcNow;
try
{
task();
return;
}
catch (Exception ex)
{
var failure = CaptureFailure(ex);
onFail(failure);
Time.Sleep(sleepAfterFail);
}
}
}
private Failure CaptureFailure(Exception ex)
{
var f = new Failure(ex, DateTime.UtcNow - tryStart, tryNumber);
failures.Add(f);
return f;
}
private void CheckMaximums()
{
if (Duration() > maxTimeout) Fail();
}
private void Fail()
{
throw new TimeoutException($"Retry '{description}' timed out after {tryNumber} tries over {Time.FormatDuration(Duration())}: {GetFailureReport}",
new AggregateException(failures.Select(f => f.Exception)));
}
private string GetFailureReport()
{
return Environment.NewLine + string.Join(Environment.NewLine, failures.Select(f => f.Describe()));
}
private TimeSpan Duration()
{
return DateTime.UtcNow - start;
}
}
}
public class Failure
{
public Failure(Exception exception, TimeSpan duration, int tryNumber)
{
Exception = exception;
Duration = duration;
TryNumber = tryNumber;
}
public Exception Exception { get; }
public TimeSpan Duration { get; }
public int TryNumber { get; }
public string Describe()
{
return $"Try {TryNumber} failed after {Time.FormatDuration(Duration)} with exception '{Exception}'";
}
}
}
+28
View File
@@ -0,0 +1,28 @@
namespace Utils
{
public static class RollingAverage
{
/// <param name="currentAverage">Value of average before new value is added.</param>
/// <param name="newNumberOfValues">Number of values in average after new value is added.</param>
/// <param name="newValue">New value to be added.</param>
/// <returns>New average value.</returns>
/// <exception cref="Exception">newNumberOfValues must be 1 or greater.</exception>
public static float GetNewAverage(float currentAverage, int newNumberOfValues, float newValue)
{
if (newNumberOfValues < 1) throw new Exception("Should be at least 1 value.");
float n = newNumberOfValues;
var originalValue = currentAverage;
var originalValueWeight = ((n - 1.0f) / n);
var newValueWeight = (1.0f / n);
return GetWeightedAverage(originalValue, originalValueWeight, newValue, newValueWeight);
}
public static float GetWeightedAverage(float value1, float weight1, float value2, float weight2)
{
float totalWeight = weight1 + weight2;
if (totalWeight == 0.0f) return 0.0f;
return ((value1 * weight1) + (value2 * weight2)) / totalWeight;
}
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace Utils
{
public static class Str
{
public static string Between(string input, string open, string close)
{
var openIndex = input.IndexOf(open) + open.Length;
var closeIndex = input.LastIndexOf(close);
return input.Substring(openIndex, closeIndex - openIndex);
}
}
}
+26 -53
View File
@@ -18,6 +18,12 @@
task.Wait();
}
public static string FormatDuration(TimeSpan? d)
{
if (d == null) return "[NULL]";
return FormatDuration(d.Value);
}
public static string FormatDuration(TimeSpan d)
{
var result = "";
@@ -83,77 +89,44 @@
public static void Retry(Action action, string description)
{
Retry(action, 1, description);
Retry(action, TimeSpan.FromSeconds(30), description);
}
public static T Retry<T>(Func<T> action, string description)
{
return Retry(action, 1, description);
return Retry(action, TimeSpan.FromSeconds(30), description);
}
public static void Retry(Action action, int maxRetries, string description)
public static void Retry(Action action, TimeSpan maxTimeout, string description)
{
Retry(action, maxRetries, TimeSpan.FromSeconds(5), description);
Retry(action, maxTimeout, TimeSpan.FromSeconds(5), description);
}
public static T Retry<T>(Func<T> action, int maxRetries, string description)
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, string description)
{
return Retry(action, maxRetries, TimeSpan.FromSeconds(5), description);
return Retry(action, maxTimeout, TimeSpan.FromSeconds(5), description);
}
public static void Retry(Action action, int maxRetries, TimeSpan retryTime, string description)
public static void Retry(Action action, TimeSpan maxTimeout, TimeSpan retryTime, string description)
{
var start = DateTime.UtcNow;
var retries = 0;
var exceptions = new List<Exception>();
while (true)
{
if (retries > maxRetries)
{
var duration = DateTime.UtcNow - start;
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
}
try
{
action();
return;
}
catch (Exception ex)
{
exceptions.Add(ex);
retries++;
}
Sleep(retryTime);
}
Retry(action, maxTimeout, retryTime, description, f => { });
}
public static T Retry<T>(Func<T> action, int maxRetries, TimeSpan retryTime, string description)
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, TimeSpan retryTime, string description)
{
var start = DateTime.UtcNow;
var retries = 0;
var exceptions = new List<Exception>();
while (true)
{
if (retries > maxRetries)
{
var duration = DateTime.UtcNow - start;
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
}
return Retry(action, maxTimeout, retryTime, description, f => { });
}
try
{
return action();
}
catch (Exception ex)
{
exceptions.Add(ex);
retries++;
}
public static void Retry(Action action, TimeSpan maxTimeout, TimeSpan retryTime, string description, Action<Failure> onFail)
{
var r = new Retry(description, maxTimeout, retryTime, onFail);
r.Run(action);
}
Sleep(retryTime);
}
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, TimeSpan retryTime, string description, Action<Failure> onFail)
{
var r = new Retry(description, maxTimeout, retryTime, onFail);
return r.Run(action);
}
}
}
@@ -0,0 +1,22 @@
using KubernetesWorkflow;
using KubernetesWorkflow.Recipe;
namespace BittorrentPlugin
{
public class BittorrentContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "bittorrent";
public override string Image => "thatbenbierens/bittorrentdriver:init12";
public static string ApiPortTag = "API_PORT";
public static string TrackerPortTag = "TRACKER_PORT";
public static string PeerPortTag = "PEER_PORT";
protected override void Initialize(StartupConfig config)
{
AddInternalPortAndVar("TRACKERPORT", TrackerPortTag);
AddInternalPortAndVar("PEERPORT", PeerPortTag);
AddExposedPortAndVar("APIPORT", ApiPortTag);
}
}
}
@@ -0,0 +1,146 @@
using Core;
using KubernetesWorkflow.Types;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Utils;
namespace BittorrentPlugin
{
public interface IBittorrentNode
{
string StartAsTracker();
string AddTracker(IBittorrentNode tracker, string localFile);
string PutFile(string base64);
string GetTrackerStats();
CreateTorrentResult CreateTorrent(ByteSize size, IBittorrentNode tracker);
string StartDaemon();
string DownloadTorrent(string LocalFile);
}
public class BittorrentNode : IBittorrentNode
{
private readonly IPluginTools tools;
private readonly RunningContainer container;
private readonly PodInfo podInfo;
public BittorrentNode(IPluginTools tools, RunningContainer container)
{
this.tools = tools;
this.container = container;
podInfo = tools.CreateWorkflow().GetPodInfo(container);
}
public string StartAsTracker()
{
//TrackerAddress = container.GetInternalAddress(BittorrentContainerRecipe.TrackerPortTag);
var endpoint = GetEndpoint();
return endpoint.HttpPutString("starttracker", GetTrackerAddress().Port.ToString());
}
public string AddTracker(IBittorrentNode tracker, string localFile)
{
var endpoint = GetEndpoint();
var trackerUrl = ((BittorrentNode)tracker).GetTrackerAddress();
return endpoint.HttpPostJson("addtracker", new AddTrackerRequest
{
LocalFile = localFile,
TrackerUrl = $"{trackerUrl}/announce"
});
}
public string PutFile(string base64)
{
var endpoint = GetEndpoint();
return endpoint.HttpPostJson("postfile", new PostFileRequest
{
Base64Content = base64
});
}
public string StartDaemon()
{
var endpoint = GetEndpoint();
var peerPortAddress = container.GetInternalAddress(BittorrentContainerRecipe.PeerPortTag);
return endpoint.HttpPutString("daemon", peerPortAddress.Port.ToString());
}
public CreateTorrentResult CreateTorrent(ByteSize size, IBittorrentNode tracker)
{
var trackerUrl = ((BittorrentNode)tracker).GetTrackerAddress();
var endpoint = GetEndpoint();
var json = endpoint.HttpPostJson("create", new CreateTorrentRequest
{
Size = Convert.ToInt32(size.SizeInBytes),
TrackerUrl = $"{trackerUrl}/announce"
});
return JsonConvert.DeserializeObject<CreateTorrentResult>(json)!;
}
public string DownloadTorrent(string localFile)
{
var endpoint = GetEndpoint();
return endpoint.HttpPostJson("download", new DownloadTorrentRequest
{
LocalFile = localFile
});
}
public string GetTrackerStats()
{
var endpoint = GetEndpoint();
return endpoint.HttpGetString("stats");
}
//public Address TrackerAddress { get; private set; } = new Address("", 0);
public Address GetTrackerAddress()
{
var address = container.GetInternalAddress(BittorrentContainerRecipe.TrackerPortTag);
return new Address("http://" + podInfo.Ip, address.Port);
}
private IEndpoint GetEndpoint()
{
var address = container.GetAddress(BittorrentContainerRecipe.ApiPortTag);
var http = tools.CreateHttp(address.ToString(), c => { });
return http.CreateEndpoint(address, "/torrent/", container.Name);
}
}
public class CreateTorrentRequest
{
public int Size { get; set; }
public string TrackerUrl { get; set; } = string.Empty;
}
public class CreateTorrentResult
{
public string LocalFilePath { get; set; } = string.Empty;
public string TorrentBase64 { get; set; } = string.Empty;
}
public class DownloadTorrentRequest
{
public string LocalFile { get; set; } = string.Empty;
}
public class AddTrackerRequest
{
public string TrackerUrl { get; set; } = string.Empty;
public string LocalFile { get; set; } = string.Empty;
}
public class PostFileRequest
{
public string Base64Content { get; set; } = string.Empty;
}
}
@@ -0,0 +1,34 @@
using Core;
using KubernetesWorkflow;
using KubernetesWorkflow.Recipe;
namespace BittorrentPlugin
{
public class BittorrentPlugin : IProjectPlugin
{
private readonly IPluginTools tools;
public BittorrentPlugin(IPluginTools tools)
{
this.tools = tools;
}
public void Announce()
{
tools.GetLog().Log("Loaded Bittorrent plugin");
}
public void Decommission()
{
}
public IBittorrentNode StartNode()
{
var flow = tools.CreateWorkflow();
var pod = flow.Start(1, new BittorrentContainerRecipe(), new StartupConfig()).WaitForOnline();
var container = pod.Containers.Single();
return new BittorrentNode(tools, container);
}
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,17 @@
using Core;
namespace BittorrentPlugin
{
public static class CoreInterfaceExtensions
{
public static IBittorrentNode StartBittorrentNode(this CoreInterface ci)
{
return Plugin(ci).StartNode();
}
private static BittorrentPlugin Plugin(CoreInterface ci)
{
return ci.GetPlugin<BittorrentPlugin>();
}
}
}
@@ -0,0 +1,73 @@
using CodexContractsPlugin.Marketplace;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
{
public class ChainEvents
{
private ChainEvents(
BlockInterval blockInterval,
Request[] requests,
RequestFulfilledEventDTO[] fulfilled,
RequestCancelledEventDTO[] cancelled,
RequestFailedEventDTO[] failed,
SlotFilledEventDTO[] slotFilled,
SlotFreedEventDTO[] slotFreed
)
{
BlockInterval = blockInterval;
Requests = requests;
Fulfilled = fulfilled;
Cancelled = cancelled;
Failed = failed;
SlotFilled = slotFilled;
SlotFreed = slotFreed;
}
public BlockInterval BlockInterval { get; }
public Request[] Requests { get; }
public RequestFulfilledEventDTO[] Fulfilled { get; }
public RequestCancelledEventDTO[] Cancelled { get; }
public RequestFailedEventDTO[] Failed { get; }
public SlotFilledEventDTO[] SlotFilled { get; }
public SlotFreedEventDTO[] SlotFreed { get; }
public IHasBlock[] All
{
get
{
var all = new List<IHasBlock>();
all.AddRange(Requests);
all.AddRange(Fulfilled);
all.AddRange(Cancelled);
all.AddRange(Failed);
all.AddRange(SlotFilled);
all.AddRange(SlotFreed);
return all.ToArray();
}
}
public static ChainEvents FromBlockInterval(ICodexContracts contracts, BlockInterval blockInterval)
{
return FromContractEvents(contracts.GetEvents(blockInterval));
}
public static ChainEvents FromTimeRange(ICodexContracts contracts, TimeRange timeRange)
{
return FromContractEvents(contracts.GetEvents(timeRange));
}
public static ChainEvents FromContractEvents(ICodexContractsEvents events)
{
return new ChainEvents(
events.BlockInterval,
events.GetStorageRequests(),
events.GetRequestFulfilledEvents(),
events.GetRequestCancelledEvents(),
events.GetRequestFailedEvents(),
events.GetSlotFilledEvents(),
events.GetSlotFreedEvents()
);
}
}
}
@@ -0,0 +1,176 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using NethereumWorkflow.BlockUtils;
using System.Numerics;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
{
public interface IChainStateChangeHandler
{
void OnNewRequest(RequestEvent requestEvent);
void OnRequestFinished(RequestEvent requestEvent);
void OnRequestFulfilled(RequestEvent requestEvent);
void OnRequestCancelled(RequestEvent requestEvent);
void OnRequestFailed(RequestEvent requestEvent);
void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex);
void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex);
}
public class RequestEvent
{
public RequestEvent(BlockTimeEntry block, IChainStateRequest request)
{
Block = block;
Request = request;
}
public BlockTimeEntry Block { get; }
public IChainStateRequest Request { get; }
}
public class ChainState
{
private readonly List<ChainStateRequest> requests = new List<ChainStateRequest>();
private readonly ILog log;
private readonly ICodexContracts contracts;
private readonly IChainStateChangeHandler handler;
public ChainState(ILog log, ICodexContracts contracts, IChainStateChangeHandler changeHandler, DateTime startUtc)
{
this.log = new LogPrefixer(log, "(ChainState) ");
this.contracts = contracts;
handler = changeHandler;
TotalSpan = new TimeRange(startUtc, startUtc);
}
public TimeRange TotalSpan { get; private set; }
public IChainStateRequest[] Requests => requests.ToArray();
public void Update()
{
Update(DateTime.UtcNow);
}
public void Update(DateTime toUtc)
{
var span = new TimeRange(TotalSpan.To, toUtc);
var events = ChainEvents.FromTimeRange(contracts, span);
Apply(events);
TotalSpan = new TimeRange(TotalSpan.From, span.To);
}
private void Apply(ChainEvents events)
{
if (events.BlockInterval.TimeRange.From < TotalSpan.From)
throw new Exception("Attempt to update ChainState with set of events from before its current record.");
log.Log($"ChainState updating: {events.BlockInterval}");
// Run through each block and apply the events to the state in order.
var span = events.BlockInterval.TimeRange.Duration;
var numBlocks = events.BlockInterval.NumberOfBlocks;
var spanPerBlock = span / numBlocks;
var eventUtc = events.BlockInterval.TimeRange.From;
for (var b = events.BlockInterval.From; b <= events.BlockInterval.To; b++)
{
var blockEvents = events.All.Where(e => e.Block.BlockNumber == b).ToArray();
ApplyEvents(b, blockEvents, eventUtc);
eventUtc += spanPerBlock;
}
}
private void ApplyEvents(ulong blockNumber, IHasBlock[] blockEvents, DateTime eventsUtc)
{
foreach (var e in blockEvents)
{
dynamic d = e;
ApplyEvent(d);
}
ApplyTimeImplicitEvents(blockNumber, eventsUtc);
}
private void ApplyEvent(Request request)
{
if (requests.Any(r => Equal(r.Request.RequestId, request.RequestId)))
throw new Exception("Received NewRequest event for id that already exists.");
var newRequest = new ChainStateRequest(log, request, RequestState.New);
requests.Add(newRequest);
handler.OnNewRequest(new RequestEvent(request.Block, newRequest));
}
private void ApplyEvent(RequestFulfilledEventDTO @event)
{
var r = FindRequest(@event.RequestId);
if (r == null) return;
r.UpdateState(@event.Block.BlockNumber, RequestState.Started);
handler.OnRequestFulfilled(new RequestEvent(@event.Block, r));
}
private void ApplyEvent(RequestCancelledEventDTO @event)
{
var r = FindRequest(@event.RequestId);
if (r == null) return;
r.UpdateState(@event.Block.BlockNumber, RequestState.Cancelled);
handler.OnRequestCancelled(new RequestEvent(@event.Block, r));
}
private void ApplyEvent(RequestFailedEventDTO @event)
{
var r = FindRequest(@event.RequestId);
if (r == null) return;
r.UpdateState(@event.Block.BlockNumber, RequestState.Failed);
handler.OnRequestFailed(new RequestEvent(@event.Block, r));
}
private void ApplyEvent(SlotFilledEventDTO @event)
{
var r = FindRequest(@event.RequestId);
if (r == null) return;
r.Hosts.Add(@event.Host, (int)@event.SlotIndex);
r.Log($"[{@event.Block.BlockNumber}] SlotFilled (host:'{@event.Host}', slotIndex:{@event.SlotIndex})");
handler.OnSlotFilled(new RequestEvent(@event.Block, r), @event.Host, @event.SlotIndex);
}
private void ApplyEvent(SlotFreedEventDTO @event)
{
var r = FindRequest(@event.RequestId);
if (r == null) return;
r.Hosts.RemoveHost((int)@event.SlotIndex);
r.Log($"[{@event.Block.BlockNumber}] SlotFreed (slotIndex:{@event.SlotIndex})");
handler.OnSlotFreed(new RequestEvent(@event.Block, r), @event.SlotIndex);
}
private void ApplyTimeImplicitEvents(ulong blockNumber, DateTime eventsUtc)
{
foreach (var r in requests)
{
if (r.State == RequestState.Started
&& r.FinishedUtc < eventsUtc)
{
r.UpdateState(blockNumber, RequestState.Finished);
handler.OnRequestFinished(new RequestEvent(new BlockTimeEntry(blockNumber, eventsUtc), r));
}
}
}
private ChainStateRequest? FindRequest(byte[] requestId)
{
var r = requests.SingleOrDefault(r => Equal(r.Request.RequestId, requestId));
if (r == null) log.Log("Unable to find request by ID!");
return r;
}
private bool Equal(byte[] a, byte[] b)
{
return a.SequenceEqual(b);
}
}
}
@@ -0,0 +1,55 @@
using GethPlugin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace CodexContractsPlugin.ChainMonitor
{
public class ChainStateChangeHandlerMux : IChainStateChangeHandler
{
public ChainStateChangeHandlerMux(params IChainStateChangeHandler[] handlers)
{
Handlers = handlers.ToList();
}
public List<IChainStateChangeHandler> Handlers { get; } = new List<IChainStateChangeHandler>();
public void OnNewRequest(RequestEvent requestEvent)
{
foreach (var handler in Handlers) handler.OnNewRequest(requestEvent);
}
public void OnRequestCancelled(RequestEvent requestEvent)
{
foreach (var handler in Handlers) handler.OnRequestCancelled(requestEvent);
}
public void OnRequestFailed(RequestEvent requestEvent)
{
foreach (var handler in Handlers) handler.OnRequestFailed(requestEvent);
}
public void OnRequestFinished(RequestEvent requestEvent)
{
foreach (var handler in Handlers) handler.OnRequestFinished(requestEvent);
}
public void OnRequestFulfilled(RequestEvent requestEvent)
{
foreach (var handler in Handlers) handler.OnRequestFulfilled(requestEvent);
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
{
foreach (var handler in Handlers) handler.OnSlotFilled(requestEvent, host, slotIndex);
}
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
foreach (var handler in Handlers) handler.OnSlotFreed(requestEvent, slotIndex);
}
}
}
@@ -0,0 +1,80 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
namespace CodexContractsPlugin.ChainMonitor
{
public interface IChainStateRequest
{
Request Request { get; }
RequestState State { get; }
DateTime ExpiryUtc { get; }
DateTime FinishedUtc { get; }
EthAddress Client { get; }
RequestHosts Hosts { get; }
}
public class ChainStateRequest : IChainStateRequest
{
private readonly ILog log;
public ChainStateRequest(ILog log, Request request, RequestState state)
{
this.log = log;
Request = request;
State = state;
ExpiryUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Expiry);
FinishedUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Ask.Duration);
Log($"[{request.Block.BlockNumber}] Created as {State}.");
Client = new EthAddress(request.Client);
Hosts = new RequestHosts();
}
public Request Request { get; }
public RequestState State { get; private set; }
public DateTime ExpiryUtc { get; }
public DateTime FinishedUtc { get; }
public EthAddress Client { get; }
public RequestHosts Hosts { get; }
public void UpdateState(ulong blockNumber, RequestState newState)
{
Log($"[{blockNumber}] Transit: {State} -> {newState}");
State = newState;
}
public void Log(string msg)
{
log.Log($"Request '{Request.Id}': {msg}");
}
}
public class RequestHosts
{
private readonly Dictionary<int, EthAddress> hosts = new Dictionary<int, EthAddress>();
public void Add(EthAddress host, int index)
{
hosts.Add(index, host);
}
public void RemoveHost(int index)
{
hosts.Remove(index);
}
public EthAddress? GetHost(int index)
{
if (!hosts.ContainsKey(index)) return null;
return hosts[index];
}
public EthAddress[] GetHosts()
{
return hosts.Values.ToArray();
}
}
}
@@ -0,0 +1,36 @@
using GethPlugin;
using System.Numerics;
namespace CodexContractsPlugin.ChainMonitor
{
public class DoNothingChainEventHandler : IChainStateChangeHandler
{
public void OnNewRequest(RequestEvent requestEvent)
{
}
public void OnRequestCancelled(RequestEvent requestEvent)
{
}
public void OnRequestFailed(RequestEvent requestEvent)
{
}
public void OnRequestFinished(RequestEvent requestEvent)
{
}
public void OnRequestFulfilled(RequestEvent requestEvent)
{
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
{
}
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
}
}
}
@@ -2,10 +2,8 @@
using GethPlugin;
using Logging;
using Nethereum.ABI;
using Nethereum.Hex.HexTypes;
using Nethereum.Util;
using NethereumWorkflow;
using NethereumWorkflow.BlockUtils;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Utils;
@@ -22,13 +20,10 @@ namespace CodexContractsPlugin
TestToken GetTestTokenBalance(IHasEthAddress owner);
TestToken GetTestTokenBalance(EthAddress ethAddress);
Request[] GetStorageRequests(BlockInterval blockRange);
ICodexContractsEvents GetEvents(TimeRange timeRange);
ICodexContractsEvents GetEvents(BlockInterval blockInterval);
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
RequestState GetRequestState(Request request);
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange);
RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange);
SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange);
SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange);
}
[JsonConverter(typeof(StringEnumConverter))]
@@ -67,7 +62,7 @@ namespace CodexContractsPlugin
public string MintTestTokens(EthAddress ethAddress, TestToken testTokens)
{
return StartInteraction().MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
return StartInteraction().MintTestTokens(ethAddress, testTokens.TstWei, Deployment.TokenAddress);
}
public TestToken GetTestTokenBalance(IHasEthAddress owner)
@@ -78,68 +73,17 @@ namespace CodexContractsPlugin
public TestToken GetTestTokenBalance(EthAddress ethAddress)
{
var balance = StartInteraction().GetBalance(Deployment.TokenAddress, ethAddress.Address);
return balance.TestTokens();
return balance.TstWei();
}
public Request[] GetStorageRequests(BlockInterval blockRange)
public ICodexContractsEvents GetEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, blockRange);
var i = StartInteraction();
return events
.Select(e =>
{
var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.RequestId = e.Event.RequestId;
return result;
})
.ToArray();
return GetEvents(gethNode.ConvertTimeRangeToBlockRange(timeRange));
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange)
public ICodexContractsEvents GetEvents(BlockInterval blockInterval)
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
return result;
}).ToArray();
}
public SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
return new CodexContractsEvents(log, gethNode, Deployment, blockInterval);
}
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
@@ -170,17 +114,6 @@ namespace CodexContractsPlugin
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
}
private BlockTimeEntry GetBlock(ulong number)
{
return gethNode.GetBlockForNumber(number);
}
private EthAddress GetEthAddressFromTransaction(string transactionHash)
{
var transaction = gethNode.GetTransaction(transactionHash);
return new EthAddress(transaction.From);
}
private ContractInteractions StartInteraction()
{
return new ContractInteractions(log, gethNode);
@@ -19,7 +19,7 @@ namespace CodexContractsPlugin
{
var config = startupConfig.Get<CodexContractsContainerConfig>();
var address = config.GethNode.StartResult.Container.GetAddress(new NullLog(), GethContainerRecipe.HttpPortTag);
var address = config.GethNode.StartResult.Container.GetAddress(GethContainerRecipe.HttpPortTag);
SetSchedulingAffinity(notIn: "false");
@@ -0,0 +1,120 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using Nethereum.Hex.HexTypes;
using NethereumWorkflow.BlockUtils;
using Utils;
namespace CodexContractsPlugin
{
public interface ICodexContractsEvents
{
BlockInterval BlockInterval { get; }
Request[] GetStorageRequests();
RequestFulfilledEventDTO[] GetRequestFulfilledEvents();
RequestCancelledEventDTO[] GetRequestCancelledEvents();
RequestFailedEventDTO[] GetRequestFailedEvents();
SlotFilledEventDTO[] GetSlotFilledEvents();
SlotFreedEventDTO[] GetSlotFreedEvents();
}
public class CodexContractsEvents : ICodexContractsEvents
{
private readonly ILog log;
private readonly IGethNode gethNode;
private readonly CodexContractsDeployment deployment;
public CodexContractsEvents(ILog log, IGethNode gethNode, CodexContractsDeployment deployment, BlockInterval blockInterval)
{
this.log = log;
this.gethNode = gethNode;
this.deployment = deployment;
BlockInterval = blockInterval;
}
public BlockInterval BlockInterval { get; }
public Request[] GetStorageRequests()
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
var i = new ContractInteractions(log, gethNode);
return events
.Select(e =>
{
var requestEvent = i.GetRequest(deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.RequestId = e.Event.RequestId;
return result;
})
.ToArray();
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents()
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents()
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public RequestFailedEventDTO[] GetRequestFailedEvents()
{
var events = gethNode.GetEvents<RequestFailedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents()
{
var events = gethNode.GetEvents<SlotFilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
return result;
}).ToArray();
}
public SlotFreedEventDTO[] GetSlotFreedEvents()
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
private BlockTimeEntry GetBlock(ulong number)
{
return gethNode.GetBlockForNumber(number);
}
private EthAddress GetEthAddressFromTransaction(string transactionHash)
{
var transaction = gethNode.GetTransaction(transactionHash);
return new EthAddress(transaction.From);
}
}
}
@@ -6,6 +6,11 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nethereum.Generators" Version="4.21.4" />
<PackageReference Include="Nethereum.Generators.Net" Version="4.21.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
<ProjectReference Include="..\GethPlugin\GethPlugin.csproj" />
@@ -1,4 +1,5 @@
using Core;
using CodexContractsPlugin.Marketplace;
using Core;
using GethPlugin;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
@@ -64,7 +65,8 @@ namespace CodexContractsPlugin
var extractor = new ContractsContainerInfoExtractor(tools.GetLog(), workflow, container);
var marketplaceAddress = extractor.ExtractMarketplaceAddress();
var abi = extractor.ExtractMarketplaceAbi();
var (abi, bytecode) = extractor.ExtractMarketplaceAbiAndByteCode();
EnsureCompatbility(abi, bytecode);
var interaction = new ContractInteractions(tools.GetLog(), gethNode);
var tokenAddress = interaction.GetTokenAddress(marketplaceAddress);
@@ -78,6 +80,18 @@ namespace CodexContractsPlugin
return new CodexContractsDeployment(marketplaceAddress, abi, tokenAddress);
}
private void EnsureCompatbility(string abi, string bytecode)
{
var expectedByteCode = MarketplaceDeploymentBase.BYTECODE.ToLowerInvariant();
if (bytecode != expectedByteCode)
{
Log("Deployed contract is incompatible with current build of CodexContracts plugin. Running self-updater...");
var selfUpdater = new SelfUpdater();
selfUpdater.Update(abi, bytecode);
}
}
private void Log(string msg)
{
tools.GetLog().Log(msg);
@@ -44,7 +44,7 @@ namespace CodexContractsPlugin
}
}
public string MintTestTokens(EthAddress address, decimal amount, string tokenAddress)
public string MintTestTokens(EthAddress address, BigInteger amount, string tokenAddress)
{
log.Debug($"{amount} -> {address} (token: {tokenAddress})");
return MintTokens(address.Address, amount, tokenAddress);
@@ -85,7 +85,7 @@ namespace CodexContractsPlugin
}
}
private string MintTokens(string account, decimal amount, string tokenAddress)
private string MintTokens(string account, BigInteger amount, string tokenAddress)
{
log.Debug($"({tokenAddress}) {amount} --> {account}");
if (string.IsNullOrEmpty(account)) throw new ArgumentException("Invalid arguments for MintTestTokens");
@@ -93,7 +93,7 @@ namespace CodexContractsPlugin
var function = new MintTokensFunction
{
Holder = account,
Amount = amount.ToBig()
Amount = amount
};
return gethNode.SendTransaction(tokenAddress, function);
@@ -31,14 +31,14 @@ namespace CodexContractsPlugin
return marketplaceAddress;
}
public string ExtractMarketplaceAbi()
public (string, string) ExtractMarketplaceAbiAndByteCode()
{
log.Debug();
var marketplaceAbi = Retry(FetchMarketplaceAbi);
if (string.IsNullOrEmpty(marketplaceAbi)) throw new InvalidOperationException("Unable to fetch marketplace artifacts from codex-contracts node. Test infra failure.");
var (abi, bytecode) = Retry(FetchMarketplaceAbiAndByteCode);
if (string.IsNullOrEmpty(abi)) throw new InvalidOperationException("Unable to fetch marketplace artifacts from codex-contracts node. Test infra failure.");
log.Debug("Got Marketplace ABI: " + marketplaceAbi);
return marketplaceAbi;
log.Debug("Got Marketplace ABI: " + abi);
return (abi, bytecode);
}
private string FetchMarketplaceAddress()
@@ -48,7 +48,7 @@ namespace CodexContractsPlugin
return marketplace!.address;
}
private string FetchMarketplaceAbi()
private (string, string) FetchMarketplaceAbiAndByteCode()
{
var json = workflow.ExecuteCommand(container, "cat", CodexContractsContainerRecipe.MarketplaceArtifactFilename);
@@ -56,19 +56,12 @@ namespace CodexContractsPlugin
var abi = artifact["abi"];
var byteCode = artifact["bytecode"];
var abiResult = abi!.ToString(Formatting.None);
var byteCodeResult = byteCode!.ToString(Formatting.None);
if (byteCodeResult
.ToLowerInvariant()
.Replace("\"", "") != MarketplaceDeploymentBase.BYTECODE.ToLowerInvariant())
{
throw new Exception("BYTECODE in CodexContractsPlugin does not match BYTECODE deployed by container. Update Marketplace.cs generated code?");
}
return abiResult;
var byteCodeResult = byteCode!.ToString(Formatting.None).ToLowerInvariant().Replace("\"", "");
return (abiResult, byteCodeResult);
}
private static string Retry(Func<string> fetch)
private static T Retry<T>(Func<T> fetch)
{
return Time.Retry(fetch, nameof(ContractsContainerInfoExtractor));
}
@@ -5,35 +5,55 @@ using Newtonsoft.Json;
namespace CodexContractsPlugin.Marketplace
{
public partial class Request : RequestBase
public interface IHasBlock
{
BlockTimeEntry Block { get; set; }
}
public partial class Request : RequestBase, IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public byte[] RequestId { get; set; }
public EthAddress ClientAddress { get { return new EthAddress(Client); } }
[JsonIgnore]
public string Id
{
get
{
return BitConverter.ToString(RequestId).Replace("-", "").ToLowerInvariant();
}
}
}
public partial class RequestFulfilledEventDTO
public partial class RequestFulfilledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class RequestCancelledEventDTO
public partial class RequestCancelledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class SlotFilledEventDTO
public partial class RequestFailedEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class SlotFilledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public EthAddress Host { get; set; }
}
public partial class SlotFreedEventDTO
public partial class SlotFreedEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
File diff suppressed because one or more lines are too long
@@ -1 +1,14 @@
This code was generated using the Nethereum code generator, here: http://playground.nethereum.com
1. Go to site -> Abi Code Gen.
1. Contract name = "Marketplace".
1. In container, get "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json".
1. Save only ABI section as new JSON. (top-level is a json array.)
1. From original JSON get byte code.
1. Put ABI JSON and byte code into site.
1. Generate.
1. From site generated code, copy `public partial class MarketplaceDeployment` and everything after it. (be considerate of namespace brackets!)
1. In Marketplace/Marketplace.cs, replace content of 'namespace CodexContractsPlugin.Marketplace'.
@@ -0,0 +1,108 @@
namespace CodexContractsPlugin
{
public class SelfUpdater
{
public void Update(string abi, string bytecode)
{
var filePath = GetMarketplaceFilePath();
var content = GenerateContent(abi, bytecode);
var contentLines = content.Split("\r\n");
var beginWith = new string[]
{
"using Nethereum.ABI.FunctionEncoding.Attributes;",
"using Nethereum.Contracts;",
"using System.Numerics;",
"",
"// Generated code, do not modify.",
"",
"#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.",
"namespace CodexContractsPlugin.Marketplace",
"{"
};
var endWith = new string[]
{
"}",
"#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable."
};
File.Delete(filePath);
File.WriteAllLines(filePath,
beginWith.Concat(
contentLines.Concat(
endWith))
);
throw new Exception("Oh no! CodexContracts were updated. Current build of CodexContractsPlugin is incompatible. " +
"But fear not! SelfUpdater.cs has automatically updated the plugin. Just rebuild and rerun and it should work. " +
"Just in case, manual update instructions are found here: 'CodexContractsPlugin/Marketplace/README.md'.");
}
private string GetMarketplaceFilePath()
{
var here = Directory.GetCurrentDirectory();
while (true)
{
var path = GetMarketplaceFile(here);
if (path != null) return path;
var parent = Directory.GetParent(here);
var up = parent?.FullName;
if (up == null || up == here) throw new Exception("Unable to locate ProjectPlugins folder. Unable to update contracts.");
here = up;
}
}
private string? GetMarketplaceFile(string root)
{
var path = Path.Combine(root, "ProjectPlugins", "CodexContractsPlugin", "Marketplace", "Marketplace.cs");
if (File.Exists(path)) return path;
return null;
}
private string GenerateContent(string abi, string bytecode)
{
var deserializer = new Nethereum.Generators.Net.GeneratorModelABIDeserialiser();
var abiModel = deserializer.DeserialiseABI(abi);
var abiCtor = abiModel.Constructor;
var c = new Nethereum.Generators.CQS.ContractDeploymentCQSMessageGenerator(abiCtor, "namespace", bytecode, "Marketplace", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
var lines = "";
lines += c.GenerateClass();
lines += "\r\n";
foreach (var eventAbi in abiModel.Events)
{
var d = new Nethereum.Generators.DTOs.EventDTOGenerator(eventAbi, "namespace", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
lines += d.GenerateClass();
lines += "\r\n";
}
foreach (var errorAbi in abiModel.Errors)
{
var e = new Nethereum.Generators.DTOs.ErrorDTOGenerator(errorAbi, "namespace", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
lines += e.GenerateClass();
lines += "\r\n";
}
foreach (var funcAbi in abiModel.Functions)
{
var f = new Nethereum.Generators.DTOs.FunctionOutputDTOGenerator(funcAbi, "namespace", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
var ff = new Nethereum.Generators.CQS.FunctionCQSMessageGenerator(funcAbi, "namespace", "funcoutput", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
lines += f.GenerateClass();
lines += "\r\n";
lines += ff.GenerateClass();
lines += "\r\n";
}
foreach (var structAbi in abiModel.Structs)
{
var g = new Nethereum.Generators.DTOs.StructTypeGenerator(structAbi, "namespace", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
lines += g.GenerateClass();
lines += "\r\n";
}
return lines;
}
}
}
@@ -1,45 +1,102 @@
namespace CodexContractsPlugin
using System.Numerics;
namespace CodexContractsPlugin
{
public class TestToken : IComparable<TestToken>
{
public TestToken(decimal amount)
public static BigInteger WeiFactor = new BigInteger(1000000000000000000);
public TestToken(BigInteger tstWei)
{
Amount = amount;
TstWei = tstWei;
Tst = tstWei / WeiFactor;
}
public decimal Amount { get; }
public BigInteger TstWei { get; }
public BigInteger Tst { get; }
public int CompareTo(TestToken? other)
{
return Amount.CompareTo(other!.Amount);
return TstWei.CompareTo(other!.TstWei);
}
public override bool Equals(object? obj)
{
return obj is TestToken token && Amount == token.Amount;
return obj is TestToken token && TstWei == token.TstWei;
}
public override int GetHashCode()
{
return HashCode.Combine(Amount);
return HashCode.Combine(TstWei);
}
public override string ToString()
{
return $"{Amount} TestTokens";
var weiOnly = TstWei % WeiFactor;
var tokens = new List<string>();
if (Tst > 0) tokens.Add($"{Tst} TST");
if (weiOnly > 0) tokens.Add($"{weiOnly} TSTWEI");
return string.Join(" + ", tokens);
}
public static TestToken operator +(TestToken a, TestToken b)
{
return new TestToken(a.TstWei + b.TstWei);
}
public static bool operator <(TestToken a, TestToken b)
{
return a.TstWei < b.TstWei;
}
public static bool operator >(TestToken a, TestToken b)
{
return a.TstWei > b.TstWei;
}
public static bool operator ==(TestToken a, TestToken b)
{
return a.TstWei == b.TstWei;
}
public static bool operator !=(TestToken a, TestToken b)
{
return a.TstWei != b.TstWei;
}
}
public static class TokensIntExtensions
public static class TestTokensExtensions
{
public static TestToken TestTokens(this int i)
public static TestToken TstWei(this int i)
{
return TestTokens(Convert.ToDecimal(i));
return TstWei(Convert.ToDecimal(i));
}
public static TestToken TestTokens(this decimal i)
public static TestToken TstWei(this decimal i)
{
return new TestToken(new BigInteger(i));
}
public static TestToken TstWei(this BigInteger i)
{
return new TestToken(i);
}
public static TestToken Tst(this int i)
{
return Tst(Convert.ToDecimal(i));
}
public static TestToken Tst(this decimal i)
{
return new TestToken(new BigInteger(i) * TestToken.WeiFactor);
}
public static TestToken Tst(this BigInteger i)
{
return new TestToken(i * TestToken.WeiFactor);
}
}
}
@@ -1,11 +1,13 @@
using Core;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
using Utils;
namespace CodexDiscordBotPlugin
{
public class CodexDiscordBotPlugin : IProjectPlugin, IHasLogPrefix, IHasMetadata
{
private const string ExpectedStartupMessage = "Debug option is set. Discord connection disabled!";
private readonly IPluginTools tools;
public CodexDiscordBotPlugin(IPluginTools tools)
@@ -46,14 +48,59 @@ namespace CodexDiscordBotPlugin
var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name;
startupConfig.Add(config);
return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline();
var pod = workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline();
WaitForStartupMessage(workflow, pod);
workflow.CreateCrashWatcher(pod.Containers.Single()).Start();
return pod;
}
private RunningPod StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config)
{
var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name;
startupConfig.Add(config);
return workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
var pod = workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
workflow.CreateCrashWatcher(pod.Containers.Single()).Start();
return pod;
}
private void WaitForStartupMessage(IStartupWorkflow workflow, RunningPod pod)
{
var finder = new LogLineFinder(ExpectedStartupMessage, workflow);
Time.WaitUntil(() =>
{
finder.FindLine(pod);
return finder.Found;
}, nameof(WaitForStartupMessage));
}
public class LogLineFinder : LogHandler
{
private readonly string message;
private readonly IStartupWorkflow workflow;
public LogLineFinder(string message, IStartupWorkflow workflow)
{
this.message = message;
this.workflow = workflow;
}
public void FindLine(RunningPod pod)
{
Found = false;
foreach (var c in pod.Containers)
{
workflow.DownloadContainerLog(c, this);
if (Found) return;
}
}
public bool Found { get; private set; }
protected override void ProcessLine(string line)
{
if (!Found && line.Contains(message)) Found = true;
}
}
}
}
@@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin
public class DiscordBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-bibliotech";
public override string Image => "codexstorage/codex-discordbot:sha-8c64352";
public override string Image => "codexstorage/codex-discordbot:sha-8033da1";
public static string RewardsPort = "bot_rewards_port";
@@ -33,6 +33,8 @@ namespace CodexDiscordBotPlugin
AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress);
AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi);
AddEnvVar("NODISCORD", "1");
AddInternalPortAndVar("REWARDAPIPORT", RewardsPort);
if (!string.IsNullOrEmpty(config.DataPath))
@@ -27,8 +27,9 @@
public class RewarderBotStartupConfig
{
public RewarderBotStartupConfig(string discordBotHost, int discordBotPort, string intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath)
public RewarderBotStartupConfig(string name, string discordBotHost, int discordBotPort, int intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath)
{
Name = name;
DiscordBotHost = discordBotHost;
DiscordBotPort = discordBotPort;
IntervalMinutes = intervalMinutes;
@@ -37,9 +38,10 @@
DataPath = dataPath;
}
public string Name { get; }
public string DiscordBotHost { get; }
public int DiscordBotPort { get; }
public string IntervalMinutes { get; }
public int IntervalMinutes { get; }
public DateTime HistoryStartUtc { get; }
public DiscordBotGethInfo GethInfo { get; }
public string? DataPath { get; set; }
@@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin
public class RewarderBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-rewarder";
public override string Image => "codexstorage/codex-rewarderbot:sha-2ab84e2";
public override string Image => "codexstorage/codex-rewarderbot:sha-8033da1";
protected override void Initialize(StartupConfig startupConfig)
{
@@ -17,7 +17,7 @@ namespace CodexDiscordBotPlugin
AddEnvVar("DISCORDBOTHOST", config.DiscordBotHost);
AddEnvVar("DISCORDBOTPORT", config.DiscordBotPort.ToString());
AddEnvVar("INTERVALMINUTES", config.IntervalMinutes);
AddEnvVar("INTERVALMINUTES", config.IntervalMinutes.ToString());
var offset = new DateTimeOffset(config.HistoryStartUtc);
AddEnvVar("CHECKHISTORY", offset.ToUnixTimeSeconds().ToString());
+1 -1
View File
@@ -9,7 +9,7 @@ namespace CodexPlugin
public class ApiChecker
{
// <INSERT-OPENAPI-YAML-HASH>
private const string OpenApiYamlHash = "5A-B0-2A-AC-42-B1-A2-49-6F-9D-4E-D8-56-40-10-A6-67-F4-0D-2A-9F-E0-84-5C-EB-B8-2D-4F-D8-56-79-6C";
private const string OpenApiYamlHash = "67-76-AB-FC-54-4F-EB-81-F5-E4-F8-27-DF-82-92-41-63-A5-EA-1B-17-14-0C-BE-20-9C-B3-DF-CE-E4-AA-38";
private const string OpenApiFilePath = "/codex/openapi.yaml";
private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK";
+141 -49
View File
@@ -2,25 +2,26 @@
using Core;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace CodexPlugin
{
public class CodexAccess : ILogHandler
public class CodexAccess
{
private readonly ILog log;
private readonly IPluginTools tools;
private readonly Mapper mapper = new Mapper();
private bool hasContainerCrashed;
public CodexAccess(IPluginTools tools, RunningPod container, CrashWatcher crashWatcher)
{
this.tools = tools;
log = tools.GetLog();
Container = container;
CrashWatcher = crashWatcher;
hasContainerCrashed = false;
CrashWatcher.Start(this);
CrashWatcher.Start();
}
public RunningPod Container { get; }
@@ -34,20 +35,23 @@ namespace CodexPlugin
public DebugPeer GetDebugPeer(string peerId)
{
// Cannot use openAPI: debug/peer endpoint is not specified there.
var endpoint = GetEndpoint();
var str = endpoint.HttpGetString($"debug/peer/{peerId}");
if (str.ToLowerInvariant() == "unable to find peer!")
return CrashCheck(() =>
{
return new DebugPeer
{
IsPeerFound = false
};
}
var endpoint = GetEndpoint();
var str = endpoint.HttpGetString($"debug/peer/{peerId}");
var result = endpoint.Deserialize<DebugPeer>(str);
result.IsPeerFound = true;
return result;
if (str.ToLowerInvariant() == "unable to find peer!")
{
return new DebugPeer
{
IsPeerFound = false
};
}
var result = endpoint.Deserialize<DebugPeer>(str);
result.IsPeerFound = true;
return result;
});
}
public void ConnectToPeer(string peerId, string[] peerMultiAddresses)
@@ -59,14 +63,19 @@ namespace CodexPlugin
});
}
public string UploadFile(FileStream fileStream)
public string UploadFile(FileStream fileStream, Action<Failure> onFailure)
{
return OnCodex(api => api.UploadAsync(fileStream));
return OnCodex(
api => api.UploadAsync(fileStream),
CreateRetryConfig(nameof(UploadFile), onFailure));
}
public Stream DownloadFile(string contentId)
public Stream DownloadFile(string contentId, Action<Failure> onFailure)
{
var fileResponse = OnCodex(api => api.DownloadNetworkAsync(contentId));
var fileResponse = OnCodex(
api => api.DownloadNetworkAsync(contentId),
CreateRetryConfig(nameof(DownloadFile), onFailure));
if (fileResponse.StatusCode != 200) throw new Exception("Download failed with StatusCode: " + fileResponse.StatusCode);
return fileResponse.Stream;
}
@@ -89,15 +98,24 @@ namespace CodexPlugin
return OnCodex<string>(api => api.CreateStorageRequestAsync(request.ContentId.Id, body));
}
public CodexSpace Space()
{
var space = OnCodex<Space>(api => api.SpaceAsync());
return mapper.Map(space);
}
public StoragePurchase GetPurchaseStatus(string purchaseId)
{
var endpoint = GetEndpoint();
return Time.Retry(() =>
return CrashCheck(() =>
{
var str = endpoint.HttpGetString($"storage/purchases/{purchaseId}");
if (string.IsNullOrEmpty(str)) throw new Exception("Empty response.");
return JsonConvert.DeserializeObject<StoragePurchase>(str)!;
}, nameof(GetPurchaseStatus));
var endpoint = GetEndpoint();
return Time.Retry(() =>
{
var str = endpoint.HttpGetString($"storage/purchases/{purchaseId}");
if (string.IsNullOrEmpty(str)) throw new Exception("Empty response.");
return JsonConvert.DeserializeObject<StoragePurchase>(str)!;
}, nameof(GetPurchaseStatus));
});
// TODO: current getpurchase api does not line up with its openapi spec.
// return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId)));
@@ -114,53 +132,127 @@ namespace CodexPlugin
return workflow.GetPodInfo(Container);
}
public void DeleteRepoFolder()
{
try
{
var containerNumber = Container.Containers.First().Recipe.Number;
var dataDir = $"datadir{containerNumber}";
var workflow = tools.CreateWorkflow();
workflow.ExecuteCommand(Container.Containers.First(), "rm", "-Rfv", $"/codex/{dataDir}/repo");
Log("Deleted repo folder.");
}
catch (Exception e)
{
Log("Unable to delete repo folder: " + e);
}
}
private T OnCodex<T>(Func<CodexApi, Task<T>> action)
{
var address = GetAddress();
var result = tools.CreateHttp(CheckContainerCrashed)
.OnClient(client =>
{
var api = new CodexApi(client);
api.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1";
return Time.Wait(action(api));
});
var result = tools.CreateHttp(GetHttpId(), CheckContainerCrashed).OnClient(client => CallCodex(client, action));
return result;
}
private T OnCodex<T>(Func<CodexApi, Task<T>> action, Retry retry)
{
var result = tools.CreateHttp(GetHttpId(), CheckContainerCrashed).OnClient(client => CallCodex(client, action), retry);
return result;
}
private T CallCodex<T>(HttpClient client, Func<CodexApi, Task<T>> action)
{
var address = GetAddress();
var api = new CodexApi(client);
api.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1";
return CrashCheck(() => Time.Wait(action(api)));
}
private T CrashCheck<T>(Func<T> action)
{
try
{
return action();
}
finally
{
CrashWatcher.HasContainerCrashed();
}
}
private IEndpoint GetEndpoint()
{
return tools
.CreateHttp(CheckContainerCrashed)
.CreateHttp(GetHttpId(), CheckContainerCrashed)
.CreateEndpoint(GetAddress(), "/api/codex/v1/", Container.Name);
}
private Address GetAddress()
{
return Container.Containers.Single().GetAddress(tools.GetLog(), CodexContainerRecipe.ApiPortTag);
return Container.Containers.Single().GetAddress(CodexContainerRecipe.ApiPortTag);
}
private string GetHttpId()
{
return GetAddress().ToString();
}
private void CheckContainerCrashed(HttpClient client)
{
if (hasContainerCrashed) throw new Exception("Container has crashed.");
if (CrashWatcher.HasContainerCrashed()) throw new Exception($"Container {GetName()} has crashed.");
}
public void Log(Stream crashLog)
private Retry CreateRetryConfig(string description, Action<Failure> onFailure)
{
var log = tools.GetLog();
var file = log.CreateSubfile();
log.Log($"Container {Container.Name} has crashed. Downloading crash log to '{file.FullFilename}'...");
file.Write($"Container Crash Log for {Container.Name}.");
var timeSet = tools.TimeSet;
using var reader = new StreamReader(crashLog);
var line = reader.ReadLine();
while (line != null)
return new Retry(description, timeSet.HttpRetryTimeout(), timeSet.HttpCallRetryDelay(), failure =>
{
file.Write(line);
line = reader.ReadLine();
onFailure(failure);
Investigate(failure, timeSet);
});
}
private void Investigate(Failure failure, ITimeSet timeSet)
{
Log($"Retry {failure.TryNumber} took {Time.FormatDuration(failure.Duration)} and failed with '{failure.Exception}'. " +
$"(HTTP timeout = {Time.FormatDuration(timeSet.HttpCallTimeout())}) " +
$"Checking if node responds to debug/info...");
try
{
var debugInfo = GetDebugInfo();
if (string.IsNullOrEmpty(debugInfo.Spr))
{
Log("Did not get value debug/info response.");
Throw(failure);
}
else
{
Log("Got valid response from debug/info.");
}
}
catch (Exception ex)
{
Log("Got exception from debug/info call: " + ex);
Throw(failure);
}
log.Log("Crash log successfully downloaded.");
hasContainerCrashed = true;
if (failure.Duration < timeSet.HttpCallTimeout())
{
Log("Retry failed within HTTP timeout duration.");
Throw(failure);
}
}
private void Throw(Failure failure)
{
throw failure.Exception;
}
private void Log(string msg)
{
log.Log($"{GetName()} {msg}");
}
}
}
@@ -7,7 +7,7 @@ namespace CodexPlugin
{
public class CodexContainerRecipe : ContainerRecipeFactory
{
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-1524803-dist-tests";
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-656ce37-dist-tests";
public const string ApiPortTag = "codex_api_port";
public const string ListenPortTag = "codex_listen_port";
public const string MetricsPortTag = "codex_metrics_port";
@@ -108,8 +108,9 @@ namespace CodexPlugin
// Custom scripting in the Codex test image will write this variable to a private-key file,
// and pass the correct filename to Codex.
AddEnvVar("PRIV_KEY", marketplaceSetup.EthAccount.PrivateKey);
Additional(marketplaceSetup.EthAccount);
var account = marketplaceSetup.EthAccountSetup.GetNew();
AddEnvVar("PRIV_KEY", account.PrivateKey);
Additional(account);
SetCommandOverride(marketplaceSetup);
if (marketplaceSetup.IsValidator)
@@ -118,7 +119,7 @@ namespace CodexPlugin
}
}
if(!string.IsNullOrEmpty(config.NameOverride))
if (!string.IsNullOrEmpty(config.NameOverride))
{
AddEnvVar("CODEX_NODENAME", config.NameOverride);
}
@@ -158,7 +159,7 @@ namespace CodexPlugin
private ByteSize GetVolumeCapacity(CodexStartupConfig config)
{
if (config.StorageQuota != null) return config.StorageQuota;
if (config.StorageQuota != null) return config.StorageQuota.Multiply(1.2);
// Default Codex quota: 8 Gb, using +20% to be safe.
return 8.GB().Multiply(1.2);
}
+105
View File
@@ -0,0 +1,105 @@
using System.Globalization;
namespace CodexPlugin
{
public class CodexLogLine
{
public static CodexLogLine? Parse(string line)
{
try
{
if (string.IsNullOrEmpty(line) ||
line.Length < 34 ||
line[3] != ' ' ||
line[33] != ' ') return null;
line = line.Replace(Environment.NewLine, string.Empty);
var level = line.Substring(0, 3);
var dtLine = line.Substring(4, 23);
var firstEqualSign = line.IndexOf('=');
var msgStart = 34;
var msgEnd = line.Substring(0, firstEqualSign).LastIndexOf(' ');
var msg = line.Substring(msgStart, msgEnd - msgStart).Trim();
var attrsLine = line.Substring(msgEnd);
var attrs = SplitAttrs(attrsLine);
var format = "yyyy-MM-dd HH:mm:ss.fff";
var dt = DateTime.ParseExact(dtLine, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToUniversalTime();
return new CodexLogLine()
{
LogLevel = level,
TimestampUtc = dt,
Message = msg,
Attributes = attrs
};
}
catch
{
return null;
}
}
public string LogLevel { get; set; } = string.Empty;
public DateTime TimestampUtc { get; set; }
public string Message { get; set; } = string.Empty;
public Dictionary<string, string> Attributes { get; private set; } = new Dictionary<string, string>();
/// <summary>
/// After too much time spent cursing at regexes, here's what I got:
/// Parses input string into 'key=value' pair, considerate of quoted (") values.
/// </summary>
private static Dictionary<string, string> SplitAttrs(string input)
{
input += " ";
var result = new Dictionary<string, string>();
var key = string.Empty;
var value = string.Empty;
var mode = 1;
var inQuote = false;
foreach (var c in input)
{
if (mode == 1)
{
if (c == '=') mode = 2;
else if (c == ' ')
{
if (string.IsNullOrEmpty(key)) continue;
else
{
result.Add(key, string.Empty);
key = string.Empty;
value = string.Empty;
}
}
else key += c;
}
else if (mode == 2)
{
if (c == ' ' && !inQuote)
{
result.Add(key, value);
key = string.Empty;
value = string.Empty;
mode = 1;
}
else if (c == '\"')
{
inQuote = !inQuote;
}
else
{
value += c;
}
}
}
return result;
}
}
}
+112 -29
View File
@@ -1,4 +1,5 @@
using Core;
using CodexPlugin.Hooks;
using Core;
using FileUtils;
using GethPlugin;
using KubernetesWorkflow;
@@ -12,36 +13,64 @@ namespace CodexPlugin
public interface ICodexNode : IHasContainer, IHasMetricsScrapeTarget, IHasEthAddress
{
string GetName();
string GetPeerId();
DebugInfo GetDebugInfo();
DebugPeer GetDebugPeer(string peerId);
ContentId UploadFile(TrackedFile file);
ContentId UploadFile(TrackedFile file, Action<Failure> onFailure);
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
TrackedFile? DownloadContent(ContentId contentId, Action<Failure> onFailure, string fileLabel = "");
LocalDatasetList LocalFiles();
CodexSpace Space();
void ConnectToPeer(ICodexNode node);
DebugInfoVersion Version { get; }
IMarketplaceAccess Marketplace { get; }
CrashWatcher CrashWatcher { get; }
PodInfo GetPodInfo();
ITransferSpeeds TransferSpeeds { get; }
EthAccount EthAccount { get; }
/// <summary>
/// Warning! The node is not usable after this.
/// TODO: Replace with delete-blocks debug call once available in Codex.
/// </summary>
void DeleteRepoFolder();
void Stop(bool waitTillStopped);
}
public class CodexNode : ICodexNode
{
private const string UploadFailedMessage = "Unable to store block";
private readonly ILog log;
private readonly IPluginTools tools;
private readonly EthAddress? ethAddress;
private readonly ICodexNodeHooks hooks;
private readonly EthAccount? ethAccount;
private readonly TransferSpeeds transferSpeeds;
private string peerId = string.Empty;
private string nodeId = string.Empty;
public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAddress? ethAddress)
public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, ICodexNodeHooks hooks, EthAccount? ethAccount)
{
this.tools = tools;
this.ethAddress = ethAddress;
this.ethAccount = ethAccount;
CodexAccess = codexAccess;
Group = group;
Marketplace = marketplaceAccess;
this.hooks = hooks;
Version = new DebugInfoVersion();
transferSpeeds = new TransferSpeeds();
log = new LogPrefixer(tools.GetLog(), $"{GetName()} ");
}
public void Awake()
{
hooks.OnNodeStarting(Container.Recipe.RecipeCreatedUtc, Container.Recipe.Image, ethAccount);
}
public void Initialize()
{
hooks.OnNodeStarted(peerId, nodeId);
}
public RunningPod Pod { get { return CodexAccess.Container; } }
@@ -66,8 +95,17 @@ namespace CodexPlugin
{
get
{
if (ethAddress == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it.");
return ethAddress;
EnsureMarketplace();
return ethAccount!.EthAddress;
}
}
public EthAccount EthAccount
{
get
{
EnsureMarketplace();
return ethAccount!;
}
}
@@ -76,11 +114,16 @@ namespace CodexPlugin
return Container.Name;
}
public string GetPeerId()
{
return peerId;
}
public DebugInfo GetDebugInfo()
{
var debugInfo = CodexAccess.GetDebugInfo();
var known = string.Join(",", debugInfo.Table.Nodes.Select(n => n.PeerId));
Log($"Got DebugInfo with id: '{debugInfo.Id}'. This node knows: {known}");
Log($"Got DebugInfo with id: {debugInfo.Id}. This node knows: [{known}]");
return debugInfo;
}
@@ -90,34 +133,55 @@ namespace CodexPlugin
}
public ContentId UploadFile(TrackedFile file)
{
return UploadFile(file, DoNothing);
}
public ContentId UploadFile(TrackedFile file, Action<Failure> onFailure)
{
using var fileStream = File.OpenRead(file.Filename);
var uniqueId = Guid.NewGuid().ToString();
var size = file.GetFilesize();
hooks.OnFileUploading(uniqueId, size);
var logMessage = $"Uploading file {file.Describe()}...";
Log(logMessage);
var measurement = Stopwatch.Measure(tools.GetLog(), logMessage, () =>
var measurement = Stopwatch.Measure(log, logMessage, () =>
{
return CodexAccess.UploadFile(fileStream);
return CodexAccess.UploadFile(fileStream, onFailure);
});
var response = measurement.Value;
transferSpeeds.AddUploadSample(file.GetFilesize(), measurement.Duration);
transferSpeeds.AddUploadSample(size, measurement.Duration);
if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response.");
if (response.StartsWith(UploadFailedMessage)) FrameworkAssert.Fail("Node failed to store block.");
Log($"Uploaded file. Received contentId: '{response}'.");
return new ContentId(response);
Log($"Uploaded file {file.Describe()}. Received contentId: '{response}'.");
var cid = new ContentId(response);
hooks.OnFileUploaded(uniqueId, size, cid);
return cid;
}
public TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "")
{
var logMessage = $"Downloading for contentId: '{contentId.Id}'...";
Log(logMessage);
return DownloadContent(contentId, DoNothing, fileLabel);
}
public TrackedFile? DownloadContent(ContentId contentId, Action<Failure> onFailure, string fileLabel = "")
{
var file = tools.GetFileManager().CreateEmptyFile(fileLabel);
var measurement = Stopwatch.Measure(tools.GetLog(), logMessage, () => DownloadToFile(contentId.Id, file));
transferSpeeds.AddDownloadSample(file.GetFilesize(), measurement);
Log($"Downloaded file {file.Describe()} to '{file.Filename}'.");
hooks.OnFileDownloading(contentId);
Log($"Downloading '{contentId}'...");
var logMessage = $"Downloaded '{contentId}' to '{file.Filename}'";
var measurement = Stopwatch.Measure(log, logMessage, () => DownloadToFile(contentId.Id, file, onFailure));
var size = file.GetFilesize();
transferSpeeds.AddDownloadSample(size, measurement);
hooks.OnFileDownloaded(size, contentId);
return file;
}
@@ -126,6 +190,11 @@ namespace CodexPlugin
return CodexAccess.LocalFiles();
}
public CodexSpace Space()
{
return CodexAccess.Space();
}
public void ConnectToPeer(ICodexNode node)
{
var peer = (CodexNode)node;
@@ -142,21 +211,25 @@ namespace CodexPlugin
return CodexAccess.GetPodInfo();
}
public void DeleteRepoFolder()
{
CodexAccess.DeleteRepoFolder();
}
public void Stop(bool waitTillStopped)
{
Log("Stopping...");
hooks.OnNodeStopping();
CrashWatcher.Stop();
Group.Stop(this, waitTillStopped);
// if (Group.Count() > 1) throw new InvalidOperationException("Codex-nodes that are part of a group cannot be " +
// "individually shut down. Use 'BringOffline()' on the group object to stop the group. This method is only " +
// "available for codex-nodes in groups of 1.");
//
// Group.BringOffline(waitTillStopped);
}
public void EnsureOnlineGetVersionResponse()
{
var debugInfo = Time.Retry(CodexAccess.GetDebugInfo, "ensure online");
var nodePeerId = debugInfo.Id;
peerId = debugInfo.Id;
nodeId = debugInfo.Table.LocalNode.NodeId;
var nodeName = CodexAccess.Container.Name;
if (!debugInfo.Version.IsValid())
@@ -164,9 +237,10 @@ namespace CodexPlugin
throw new Exception($"Invalid version information received from Codex node {GetName()}: {debugInfo.Version}");
}
var log = tools.GetLog();
log.AddStringReplace(nodePeerId, nodeName);
log.AddStringReplace(peerId, nodeName);
log.AddStringReplace(CodexUtils.ToShortId(peerId), nodeName);
log.AddStringReplace(debugInfo.Table.LocalNode.NodeId, nodeName);
log.AddStringReplace(CodexUtils.ToShortId(debugInfo.Table.LocalNode.NodeId), nodeName);
Version = debugInfo.Version;
}
@@ -182,12 +256,12 @@ namespace CodexPlugin
.ToArray();
}
private void DownloadToFile(string contentId, TrackedFile file)
private void DownloadToFile(string contentId, TrackedFile file, Action<Failure> onFailure)
{
using var fileStream = File.OpenWrite(file.Filename);
try
{
using var downloadStream = CodexAccess.DownloadFile(contentId);
using var downloadStream = CodexAccess.DownloadFile(contentId, onFailure);
downloadStream.CopyTo(fileStream);
}
catch
@@ -197,9 +271,18 @@ namespace CodexPlugin
}
}
private void EnsureMarketplace()
{
if (ethAccount == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it.");
}
private void Log(string msg)
{
tools.GetLog().Log($"{GetName()}: {msg}");
log.Log(msg);
}
private void DoNothing(Failure failure)
{
}
}
}
+15 -10
View File
@@ -1,4 +1,5 @@
using Core;
using CodexPlugin.Hooks;
using Core;
using GethPlugin;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
@@ -14,30 +15,34 @@ namespace CodexPlugin
public class CodexNodeFactory : ICodexNodeFactory
{
private readonly IPluginTools tools;
private readonly CodexHooksFactory codexHooksFactory;
public CodexNodeFactory(IPluginTools tools)
public CodexNodeFactory(IPluginTools tools, CodexHooksFactory codexHooksFactory)
{
this.tools = tools;
this.codexHooksFactory = codexHooksFactory;
}
public CodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group)
{
var ethAddress = GetEthAddress(access);
var marketplaceAccess = GetMarketplaceAccess(access, ethAddress);
return new CodexNode(tools, access, group, marketplaceAccess, ethAddress);
var ethAccount = GetEthAccount(access);
var hooks = codexHooksFactory.CreateHooks(access.Container.Name);
var marketplaceAccess = GetMarketplaceAccess(access, ethAccount, hooks);
return new CodexNode(tools, access, group, marketplaceAccess, hooks, ethAccount);
}
private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAddress? ethAddress)
private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAccount? ethAccount, ICodexNodeHooks hooks)
{
if (ethAddress == null) return new MarketplaceUnavailable();
return new MarketplaceAccess(tools.GetLog(), codexAccess);
if (ethAccount == null) return new MarketplaceUnavailable();
return new MarketplaceAccess(tools.GetLog(), codexAccess, hooks);
}
private EthAddress? GetEthAddress(CodexAccess access)
private EthAccount? GetEthAccount(CodexAccess access)
{
var ethAccount = access.Container.Containers.Single().Recipe.Additionals.Get<EthAccount>();
if (ethAccount == null) return null;
return ethAccount.EthAddress;
return ethAccount;
}
public CrashWatcher CreateCrashWatcher(RunningContainer c)
+4 -1
View File
@@ -79,13 +79,16 @@ namespace CodexPlugin
}
Version = first;
foreach (var node in Nodes) node.Initialize();
}
private CodexNode CreateOnlineCodexNode(RunningPod c, IPluginTools tools, ICodexNodeFactory factory)
{
var watcher = factory.CreateCrashWatcher(c.Containers.Single());
var access = new CodexAccess(tools, c, watcher);
return factory.CreateOnlineCodexNode(access, this);
var node = factory.CreateOnlineCodexNode(access, this);
node.Awake();
return node;
}
}
}
@@ -1,3 +1,4 @@
using CodexPlugin.Hooks;
using Core;
using KubernetesWorkflow.Types;
@@ -57,6 +58,11 @@ namespace CodexPlugin
}
}
public void SetCodexHooksProvider(ICodexHooksProvider hooksProvider)
{
codexStarter.HooksFactory.Provider = hooksProvider;
}
private CodexSetup GetSetup(int numberOfNodes, Action<ICodexSetup> setup)
{
var codexSetup = new CodexSetup(numberOfNodes);
@@ -29,6 +29,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
<ProjectReference Include="..\..\Framework\KubernetesWorkflow\KubernetesWorkflow.csproj" />
<ProjectReference Include="..\..\Framework\OverwatchTranscript\OverwatchTranscript.csproj" />
<ProjectReference Include="..\CodexContractsPlugin\CodexContractsPlugin.csproj" />
<ProjectReference Include="..\GethPlugin\GethPlugin.csproj" />
<ProjectReference Include="..\MetricsPlugin\MetricsPlugin.csproj" />
+38 -5
View File
@@ -52,6 +52,7 @@ namespace CodexPlugin
public CodexLogLevel Libp2p { get; set; }
public CodexLogLevel ContractClock { get; set; } = CodexLogLevel.Warn;
public CodexLogLevel? BlockExchange { get; }
public CodexLogLevel JsonSerialize { get; set; } = CodexLogLevel.Warn;
}
public class CodexSetup : CodexStartupConfig, ICodexSetup
@@ -167,8 +168,8 @@ namespace CodexPlugin
public bool IsStorageNode { get; private set; }
public bool IsValidator { get; private set; }
public Ether InitialEth { get; private set; } = 0.Eth();
public TestToken InitialTestTokens { get; private set; } = 0.TestTokens();
public EthAccount EthAccount { get; private set; } = EthAccount.GenerateNew();
public TestToken InitialTestTokens { get; private set; } = 0.Tst();
public EthAccountSetup EthAccountSetup { get; } = new EthAccountSetup();
public IMarketplaceSetup AsStorageNode()
{
@@ -184,7 +185,7 @@ namespace CodexPlugin
public IMarketplaceSetup WithAccount(EthAccount account)
{
EthAccount = account;
EthAccountSetup.Pin(account);
return this;
}
@@ -200,10 +201,42 @@ namespace CodexPlugin
var result = "[(clientNode)"; // When marketplace is enabled, being a clientNode is implicit.
result += IsStorageNode ? "(storageNode)" : "()";
result += IsValidator ? "(validator)" : "() ";
result += $"Address: '{EthAccount.EthAddress}' ";
result += $"InitialEth/TT({InitialEth.Eth}/{InitialTestTokens.Amount})";
result += $"Pinned address: '{EthAccountSetup}' ";
result += $"{InitialEth} / {InitialTestTokens}";
result += "] ";
return result;
}
}
public class EthAccountSetup
{
private readonly List<EthAccount> accounts = new List<EthAccount>();
private bool pinned = false;
public void Pin(EthAccount account)
{
accounts.Add(account);
pinned = true;
}
public EthAccount GetNew()
{
if (pinned) return accounts.Last();
var a = EthAccount.GenerateNew();
accounts.Add(a);
return a;
}
public EthAccount[] GetAll()
{
return accounts.ToArray();
}
public override string ToString()
{
if (!accounts.Any()) return "NoEthAccounts";
return string.Join(",", accounts.Select(a => a.ToString()).ToArray());
}
}
}
+16 -4
View File
@@ -1,4 +1,6 @@
using Core;
using CodexPlugin.Hooks;
using Core;
using GethPlugin;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
using Logging;
@@ -19,6 +21,8 @@ namespace CodexPlugin
apiChecker = new ApiChecker(pluginTools);
}
public CodexHooksFactory HooksFactory { get; } = new CodexHooksFactory();
public RunningPod[] BringOnline(CodexSetup codexSetup)
{
LogSeparator();
@@ -33,8 +37,9 @@ namespace CodexPlugin
foreach (var rc in containers)
{
var podInfo = GetPodInfo(rc);
var podInfos = string.Join(", ", rc.Containers.Select(c => $"Container: '{c.Name}' runs at '{podInfo.K8SNodeName}'={podInfo.Ip}"));
Log($"Started {codexSetup.NumberOfNodes} nodes of image '{containers.First().Containers.First().Recipe.Image}'. ({podInfos})");
var podInfos = string.Join(", ", rc.Containers.Select(c => $"Container: '{c.Name}' PodLabel: '{c.RunningPod.StartResult.Deployment.PodLabel}' runs at '{podInfo.K8SNodeName}'={podInfo.Ip}"));
Log($"Started node with image '{containers.First().Containers.First().Recipe.Image}'. ({podInfos})");
LogEthAddress(rc);
}
LogSeparator();
@@ -43,7 +48,7 @@ namespace CodexPlugin
public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningPod[] containers)
{
var codexNodeFactory = new CodexNodeFactory(pluginTools);
var codexNodeFactory = new CodexNodeFactory(pluginTools, HooksFactory);
var group = CreateCodexGroup(coreInterface, containers, codexNodeFactory);
@@ -141,6 +146,13 @@ namespace CodexPlugin
Log("----------------------------------------------------------------------------");
}
private void LogEthAddress(RunningPod rc)
{
var account = rc.Containers.First().Recipe.Additionals.Get<EthAccount>();
if (account == null) return;
Log($"{rc.Name} = {account}");
}
private void Log(string message)
{
pluginTools.GetLog().Log(message);
@@ -8,7 +8,7 @@ namespace CodexPlugin
public string? NameOverride { get; set; }
public ILocation Location { get; set; } = KnownLocations.UnspecifiedLocation;
public CodexLogLevel LogLevel { get; set; }
public CodexLogCustomTopics? CustomTopics { get; set; } = new CodexLogCustomTopics(CodexLogLevel.Warn, CodexLogLevel.Warn);
public CodexLogCustomTopics? CustomTopics { get; set; } = new CodexLogCustomTopics(CodexLogLevel.Info, CodexLogLevel.Warn);
public ByteSize? StorageQuota { get; set; }
public bool MetricsEnabled { get; set; }
public MarketplaceInitialConfig? MarketplaceConfig { get; set; }
@@ -50,10 +50,10 @@ namespace CodexPlugin
"secure",
"chronosstream",
"connection",
"connmanager",
// Removed: "connmanager", is used for transcript peer-dropped event.
"websock",
"ws-session",
"dialer",
// Removed: "dialer", is used for transcript successful-dial event.
"muxedupgrade",
"upgrade",
"identify"
@@ -73,11 +73,18 @@ namespace CodexPlugin
"contracts",
"clock"
};
var jsonSerializeTopics = new[]
{
"serde",
"json",
"serialization"
};
level = $"{level};" +
$"{CustomTopics.DiscV5.ToString()!.ToLowerInvariant()}:{string.Join(",", discV5Topics)};" +
$"{CustomTopics.Libp2p.ToString()!.ToLowerInvariant()}:{string.Join(",", libp2pTopics)};" +
$"{CustomTopics.ContractClock.ToString().ToLowerInvariant()}:{string.Join(",", contractClockTopics)}";
$"{CustomTopics.ContractClock.ToString().ToLowerInvariant()}:{string.Join(",", contractClockTopics)};" +
$"{CustomTopics.JsonSerialize.ToString().ToLowerInvariant()}:{string.Join(",", jsonSerializeTopics)}";
if (CustomTopics.BlockExchange != null)
{
+19
View File
@@ -95,6 +95,11 @@ namespace CodexPlugin
public string Id { get; }
public override string ToString()
{
return Id;
}
public override bool Equals(object? obj)
{
return obj is ContentId id && Id == id.Id;
@@ -105,4 +110,18 @@ namespace CodexPlugin
return HashCode.Combine(Id);
}
}
public class CodexSpace
{
public long TotalBlocks { get; set; }
public long QuotaMaxBytes { get; set; }
public long QuotaUsedBytes { get; set; }
public long QuotaReservedBytes { get; set; }
public long FreeBytes => QuotaMaxBytes - (QuotaUsedBytes + QuotaReservedBytes);
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
}
+24
View File
@@ -0,0 +1,24 @@
namespace CodexPlugin
{
public static class CodexUtils
{
public static string ToShortId(string id)
{
if (id.Length > 10)
{
return $"{id[..3]}*{id[^6..]}";
}
return id;
}
// after update of codex-dht, shortID should be consistent!
public static string ToNodeIdShortId(string id)
{
if (id.Length > 10)
{
return $"{id[..2]}*{id[^6..]}";
}
return id;
}
}
}
@@ -1,4 +1,5 @@
using Core;
using CodexPlugin.Hooks;
using Core;
using KubernetesWorkflow.Types;
namespace CodexPlugin
@@ -38,6 +39,11 @@ namespace CodexPlugin
return ci.StartCodexNodes(number, s => { });
}
public static void SetCodexHooksProvider(this CoreInterface ci, ICodexHooksProvider hooksProvider)
{
Plugin(ci).SetCodexHooksProvider(hooksProvider);
}
private static CodexPlugin Plugin(CoreInterface ci)
{
return ci.GetPlugin<CodexPlugin>();
@@ -0,0 +1,71 @@
using GethPlugin;
using Utils;
namespace CodexPlugin.Hooks
{
public interface ICodexHooksProvider
{
ICodexNodeHooks CreateHooks(string nodeName);
}
public class CodexHooksFactory
{
public ICodexHooksProvider Provider { get; set; } = new DoNothingHooksProvider();
public ICodexNodeHooks CreateHooks(string nodeName)
{
return Provider.CreateHooks(nodeName);
}
}
public class DoNothingHooksProvider : ICodexHooksProvider
{
public ICodexNodeHooks CreateHooks(string nodeName)
{
return new DoNothingCodexHooks();
}
}
public class DoNothingCodexHooks : ICodexNodeHooks
{
public void OnFileDownloaded(ByteSize size, ContentId cid)
{
}
public void OnFileDownloading(ContentId cid)
{
}
public void OnFileUploaded(string uid, ByteSize size, ContentId cid)
{
}
public void OnFileUploading(string uid, ByteSize size)
{
}
public void OnNodeStarted(string peerId, string nodeId)
{
}
public void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount)
{
}
public void OnNodeStopping()
{
}
public void OnStorageAvailabilityCreated(StorageAvailability response)
{
}
public void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract)
{
}
public void OnStorageContractUpdated(StoragePurchase purchaseStatus)
{
}
}
}

Some files were not shown because too many files have changed in this diff Show More