Compare commits

..
Author SHA1 Message Date
Ben 96510df5cf Fixes too-frequent save of overview json. Resets failure count on upload. 2025-03-04 16:14:29 +01:00
Ben 07c3e5fc18 Adds proofSubmitted event 2025-03-04 15:58:45 +01:00
Ben d6cd7762c4 Adds proof-period report to chain-events. 2025-03-04 15:24:25 +01:00
Ben c5de3dcb4c Fixes whitespace issue with geth env vars 2025-03-04 10:33:22 +01:00
Ben 37b98dbb05 adds balance checker to autoclient foldersaver 2025-03-04 10:03:55 +01:00
ThatBen 288ad81faa sets timeout for contract submits to equal expiry 2025-03-03 18:56:18 +01:00
ThatBen 64e917f3c2 removes "codex" from warn-level log list 2025-03-03 17:12:02 +01:00
ThatBen f33e343432 Sets log level at autoclient startup 2025-03-03 16:44:04 +01:00
ThatBen 855b627823 handles exception in GetPurchaseStatus 2025-03-03 16:27:10 +01:00
ThatBen f58a42a1ca Removes waitForSubmitted from RequestStorage call. 2025-03-03 15:56:52 +01:00
ThatBen 789852bf1a Updates api. sets marketplace-infra log level 2025-03-03 15:46:43 +01:00
ThatBen 5c56e4796f Debugs local file fetch 2025-02-28 16:45:14 +01:00
ThatBen 5dacffa720 debugging local files 2025-02-28 16:40:35 +01:00
ThatBen 9629060570 debugging resume-wait 2025-02-28 16:36:31 +01:00
ThatBen 7e37299deb immediate saves, resumable waiting 2025-02-28 16:19:26 +01:00
ThatBen eaa69bcc95 Logs duration of creating a storage request 2025-02-27 12:16:31 +01:00
ThatBen df5efa4dde Tests for local data expiry 2025-02-27 11:52:50 +01:00
ThatBen 320865460d Log quota when checking 2025-02-27 11:04:04 +01:00
ThatBen a96a9ce319 Considers local quota before uploading. 2025-02-27 10:53:35 +01:00
ThatBen 3ab1e2ea47 Use local-files api instead of download-manifest to detect existing files. 2025-02-27 10:18:35 +01:00
ThatBen 9b432ded46 oops forgot to save folderStatus after padding. 2025-02-27 10:09:03 +01:00
ThatBen e862094014 Tracks whether work item has changed. 2025-02-27 09:22:57 +01:00
ThatBen d701e2a73d Applies padding for folder file. Adds stats. 2025-02-27 09:15:28 +01:00
Ben 22e37c15e5 sets entry filename 2025-02-26 16:24:11 +01:00
Ben f7de11e9c2 Cleanup foldersaver mode 2025-02-26 16:18:42 +01:00
Ben 278b6c95c2 Sets autoclient long webcall timeout 2025-02-26 14:05:41 +01:00
Ben fe75609ecb Fix cid formatting in bot. Fix autoclient folder-uploader crash. Fix several openapi alignments. 2025-02-26 11:59:50 +01:00
Ben 3af71b1f66 Fixes marketplace api mismatch 2025-02-25 15:51:14 +01:00
Slava 3cd989c04a ci: use common docker reusable workflow (#119) 2025-02-25 08:24:02 +02:00
ThatBen 804423ec81 enables marketplace access for codex node used in autoclient 2025-02-22 14:41:05 +01:00
ThatBen 92a5a1e361 fixes crashwatcher stop 2025-02-21 15:21:42 +01:00
ThatBen 0c961b8348 updates marketplace contract, fixes type conversions 2025-02-21 14:17:13 +01:00
ThatBen dc7a034566 Merge branch 'feature/containerless-codex'
# Conflicts:
#	Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs
2025-02-21 13:58:27 +01:00
ThatBen 3459547f20 update 2025-02-21 13:57:25 +01:00
ThatBen 5d9247f82f Updates marketplace contracts 2025-02-19 12:36:29 +01:00
Ben 9e80b650c5 updates marketplace 2025-02-18 10:18:02 +01:00
Ben ce6cbf59d2 Updates codex API 2025-02-12 13:55:55 +01:00
Ben 23fdc2d60a Fixes marketplace release test 2025-02-04 13:52:30 +01:00
Ben fee42c785e Fixes collatoral issue for marketplace test 2025-02-04 10:13:59 +01:00
Ben 02d1292acc Updates contract 2025-02-04 09:39:38 +01:00
Ben 1e035d6a68 Faster binary-process log writing 2025-02-03 10:47:37 +01:00
Ben 413a46c761 Fixes namespaces for experimental tests 2025-02-03 10:08:29 +01:00
ThatBen c1e3e8be09 testing in progress 2025-01-31 14:24:55 +01:00
ThatBen 0784804fa4 workaround for downloading codex logs 2025-01-29 14:44:19 +01:00
ThatBen 8969e9a080 Merge branch 'master' into feature/containerless-codex 2025-01-29 10:26:28 +01:00
ThatBen a62c6f5108 Setup single and multiple dataset test plans 2025-01-29 10:26:16 +01:00
ThatBen e24d5e02f2 Fixes build for continuous tests 2025-01-28 15:29:25 +01:00
ThatBen 50e58dc799 cleanup on startup 2025-01-28 14:58:37 +01:00
ThatBen b1197975a1 cleanup process and datadir after test stop 2025-01-28 11:21:47 +01:00
ThatBen e4512438ed working two-client test with binary instances 2025-01-27 15:45:46 +01:00
ThatBen 2c549c5410 extracts codex wrapper 2025-01-27 10:22:34 +01:00
ThatBen d6afc6874b Applies codexStarter interface 2025-01-27 09:59:19 +01:00
ThatBen 940b8122e7 Replaces Pod types on codex plugin and starter interface 2025-01-27 09:49:21 +01:00
ThatBen 2586f4a756 update 2025-01-25 14:11:50 +01:00
ThatBen 951da1888d Merge branch 'master' into feature/containerless-codex
# Conflicts:
#	Tools/AutoClient/CodexInstance.cs
2025-01-25 14:07:41 +01:00
ThatBen caa0423bd5 Updates codex api 2025-01-25 14:07:15 +01:00
ThatBen 064fc241cb classname for generated openapi client 2025-01-16 15:16:00 +01:00
ThatBen 1a8b7b79ef wip 2025-01-16 15:13:16 +01:00
ThatBen 9a227b3d0e lots of using-statement fixes 2025-01-16 13:51:29 +01:00
ThatBen c73fa186fc wip 2025-01-16 13:24:57 +01:00
ThatBen 4a151880d4 Extracts codexClient assembly 2025-01-16 11:31:50 +01:00
ThatBen 48f4614e5d working container log connection 2025-01-16 10:21:03 +01:00
ThatBen ec644eed4a Moving downloadedlog to logging module 2025-01-16 10:15:02 +01:00
ThatBen 0c7b8fb33e debugs rewiring 2025-01-15 16:24:36 +01:00
ThatBen 0aac2d8e98 wired up except for container log access 2025-01-15 16:05:57 +01:00
ThatBen ee0193c879 wiring up started 2025-01-15 15:43:50 +01:00
ThatBen e45ed0c21e pushes container concepts out of codexAccess 2025-01-15 15:00:25 +01:00
ThatBen 2292d2e672 Removes container-related concepts from CodexNode type 2025-01-15 14:15:40 +01:00
ThatBen 8ef190baad Fixes incorrect float formatting in metricsquery. 2025-01-15 10:40:57 +01:00
Ben 9461968bd3 Disables contract-failed market test. 2025-01-13 14:03:55 +01:00
benbierens 01cefcc008 Fixes mapping of announce addresses 2025-01-08 11:52:48 +01:00
benbierens d13f6c94d3 Updates openapi.yaml 2025-01-08 11:11:28 +01:00
Ben 4801e33986 sets up combinatorials for swarm test 2024-12-18 12:54:12 +01:00
Ben 5b42b764fb Adds streamless small-swarm test 2024-12-18 12:47:54 +01:00
Ben 2afcb92d08 removes combinatorial args from marketplace tests 2024-12-18 09:28:10 +01:00
Ben a4ba1fbe2c Restores parallel settings, fixes filename test-args formatting 2024-12-18 09:21:45 +01:00
Ben 8ee5eb5767 might have been false detection 2024-12-17 16:07:01 +01:00
Ben 70622cf923 connects blockCaches from test lifecycle 2024-12-17 16:06:06 +01:00
Ben 459cb2e981 setting up for support for parallel marketplace-enabled tests 2024-12-17 15:54:52 +01:00
Ben 95aa6fd4b2 Merge branch 'feature/folder-saver' 2024-12-16 15:03:50 +01:00
Ben 1d9f22c96d Creates new overview after 3 new changes. 2024-12-16 15:03:39 +01:00
Slava c24861e932 ci: run-release-tests workflow adjustment (#108) (#118) 2024-12-16 15:30:51 +02:00
benbierens 8274c4da14 updates marketplace contracts 2024-12-13 09:35:42 +01:00
Ben e45b8bde54 adds container names to container log filenames 2024-12-09 15:57:10 +01:00
Ben 1af216592a Rewarder bot now sends initialization message to events channel 2024-12-09 10:17:47 +01:00
Ben 37f037c7b0 Fixes giving of altruistic role 2024-12-09 10:04:33 +01:00
benbierens e19311cef9 Downloads 1 file per node at a time in swarm test. 2024-12-05 11:29:21 +01:00
benbierens b8476f697e Rainbow emojis for request-IDs! 2024-12-05 08:56:01 +01:00
Ben 3e2cad3c17 Fixes issue where occassionally a CID contains 'ERR' and fails the log assert. 2024-12-04 15:57:49 +01:00
Slava 770ba6dcdc Pass repository and branch to release tests workflow (#108) (#117) 2024-12-04 09:30:31 +02:00
Slava 5cecb63307 Add workflow_source to the inputs when workflow is called (#116) 2024-12-03 14:59:28 +02:00
Ben 33153a2b76 Applies mutex in codex CidChecker 2024-12-03 08:50:50 +01:00
Slava ac8f01c922 Fix job status check (#115)
* Fix job status check

* Add a delay before get Kubernetes job status
2024-12-02 17:30:48 +02:00
Slava 4ae17c5b1d Make kubectl logs redundant for connection failure (#114) 2024-12-02 16:38:02 +02:00
Ben 35e9dd5894 Reduces swarm size for smallswarm test 2024-12-02 15:37:50 +01:00
Slava abf297263c Use variable name properly (#113) 2024-12-02 15:43:45 +02:00
Ben ed764bc38c wip 2024-12-02 14:04:17 +01:00
Slava 047384a7af Set workflow status from Kubernetes job (#108) (#112) 2024-12-02 14:59:57 +02:00
Ben 03c6b287cf Adds how to run tests 2024-12-02 10:57:07 +01:00
Ben d7c3fe6c5f Fixes PeerTableTests of CodexReleaseTests 2024-12-02 10:34:09 +01:00
Slava 86d5ab22f6 Remove timestamp from Release tests logs (#111) 2024-11-29 17:31:30 +02:00
benbierens 4bcdbf3e8c Only check CID when purchase needs to be made 2024-11-29 16:22:49 +01:00
benbierens f2442fc032 disables location selection for now 2024-11-29 09:24:43 +01:00
Ben Bierens 694b16fdd6 Merge pull request #110 from codex-storage/feature/release-tests-cleanup
Release tests cleanup
2024-11-29 09:21:32 +01:00
benbierens 10b01362e0 Merge remote-tracking branch 'origin/feature/bot-assigns-altruisticmode-role' 2024-11-29 08:52:38 +01:00
Ben Bierens 7989cdd1a2 Merge pull request #106 from hackyguru/master
Bot assigning 'Altruistic mode' role to user if the CID is valid and unique
2024-11-29 08:50:33 +01:00
Slava 58ed7ad41d Add Release tests workflow and job template (#108) 2024-11-28 20:15:38 +02:00
Slava 8d5f68609a Adjust Docker entrypoint (#108) 2024-11-28 19:57:28 +02:00
Slava b5fd8954ba Rename d-tests and c-tests jobs templates (#108) 2024-11-28 19:56:32 +02:00
Ben f84bba801b Renames codexTests to experimentalTests 2024-11-28 09:42:58 +01:00
Ben 513853d929 Better safe-duration 2024-11-27 13:43:38 +01:00
benbierens a40d5d77d7 Slow down logging when waiting for requests to start. 2024-11-27 11:35:56 +01:00
benbierens 23622245f0 cleaner log 2024-11-27 11:26:19 +01:00
benbierens f2b84ebfd6 Stores encoded-CID in worker state 2024-11-27 11:09:47 +01:00
benbierens 9853a0b7db Limits number of concurrently starting contracts 2024-11-27 10:44:05 +01:00
benbierens 025c85c1aa fixes file-backed worker issue 2024-11-27 10:30:30 +01:00
benbierens f0cbc0a53a Debugging worker files 2024-11-27 10:25:52 +01:00
benbierens e91a574b2c writes overview CID to file 2024-11-27 10:18:33 +01:00
Ben f18ff24bb6 savestate 2024-11-26 16:23:56 +01:00
Ben 529d48a758 show index 2024-11-26 16:22:43 +01:00
Ben d136345df4 sleep 2024-11-26 16:19:54 +01:00
Ben a3b9e7bf8d Uploads zipfile with archive overview periodically 2024-11-26 16:15:28 +01:00
Ben 6f778ec04f Fixes re-purchase issue 2024-11-26 15:57:34 +01:00
Ben 4b7ceda572 Split to files 2024-11-26 15:14:31 +01:00
Ben 797dc096da Wraps up market tests for now 2024-11-26 14:52:24 +01:00
Ben 208cd2e8dc Replaces CheckHistory config of rewarder bot with RelativeHistory with sane default for testnet. 2024-11-26 14:49:22 +01:00
Ben db46a0c686 Logs at start of ensure-calls 2024-11-26 12:37:36 +01:00
Ben c2df15436f Merge branch 'master' into feature/folder-saver 2024-11-26 12:27:47 +01:00
Ben dfa2322127 Setting up multiple-contracts test 2024-11-25 16:10:17 +01:00
Ben 92d504ef9c Sets up github workflow for running release tests 2024-11-25 15:57:57 +01:00
Ben 4990bb2282 contract successful test passed 2024-11-25 15:45:09 +01:00
benbierens d032b77abe wip: withdraw funds 2024-11-22 16:09:18 +01:00
benbierens e7a59de207 wip 2024-11-22 08:57:50 +01:00
Ben fd3567347b setting up marketplace tests 2024-11-21 15:30:56 +01:00
Ben f67e67c493 Setting up market tests 2024-11-21 15:10:53 +01:00
Ben 339cf2b824 Peer table test 2024-11-21 14:34:13 +01:00
Ben e743a1cd7b basic info tests 2024-11-21 14:17:57 +01:00
Ben 7c4ad416d4 running data tests 2024-11-21 12:15:15 +01:00
Ben cfb6297357 Implements streamless and manifestonly calls 2024-11-21 11:28:18 +01:00
Ben f1f3b0f173 streamelss and manifestonly tests 2024-11-21 11:21:41 +01:00
Ben 704847001d Sets up data tests 2024-11-21 10:46:11 +01:00
Ben f4c622a1d3 setting up 2024-11-21 10:03:09 +01:00
Ben 3c7892e4be Switches to latest, updates API 2024-11-21 09:39:28 +01:00
benbierens 833421b2b2 workaround for faulty local-dataset api match 2024-11-12 16:51:32 +01:00
benbierens 55cc0ab0ef sets http client timeout to 1h 2024-11-11 16:21:58 +01:00
benbierens 0129af6fd7 for vm running 2024-11-11 16:12:25 +01:00
benbierens d48caa44d6 stops autoclient after 5 failures 2024-11-11 13:41:02 +01:00
Ben e0755a1101 Updates to 0.1.8. Fixes issue in blockchain monitor 2024-11-04 12:21:39 +01:00
Ben Bierens 1dd17037ba Merge pull request #107 from codex-storage/feat/switch-docker-variable
Use ETH_PRIVATE_KEY variable instead of deprecated PRIV_KEY
2024-11-04 12:02:55 +01:00
Slava 22e6439731 Use ETH_PRIVATE_KEY variable instead of deprecated PRIV_KEY
https://github.com/codex-storage/nim-codex/pull/982
2024-11-04 12:36:25 +02:00
Kumaraguru 693880069b improving codereadability and reducing redundant file operations 2024-11-01 14:43:29 +00:00
benbierens 365032978b better status logging, faster revisiting 2024-11-01 11:14:07 +01:00
Kumaraguru eb2e61938e providing user with altrustic mode role after verifying if the cid being checked is unique 2024-11-01 01:59:28 +00:00
benbierens 5c1ffbb8af Updates codex contracts 2024-10-31 08:52:07 +01:00
Ben 3e12baaafe debug 2024-10-30 14:32:49 +01:00
Ben 605bb6411f logging 2024-10-30 11:43:57 +01:00
Ben 2554645abc debug 2024-10-30 11:38:30 +01:00
Ben 2dfcf20ecd rigged for local running on eth mainnet history 2024-10-30 11:15:33 +01:00
Ben c35784c90f Implements folder-storing 2024-10-30 11:09:13 +01:00
Ben b54c9ff9a3 Retrofitting autoclient to support folder-uploader mode 2024-10-30 08:56:31 +01:00
Ben ff4711e802 sets content-type correctly 2024-10-30 08:40:46 +01:00
Ben b8d6ac929b Update to new codex image 2024-10-30 08:34:41 +01:00
Ben acb0bf4f29 Updates to latest 2024-10-29 14:02:00 +01:00
Ben 8fe0bd6307 Applies http-timeout to download stream 2024-10-25 12:39:10 +02:00
benbierens e6a5838b05 Loops in metrics from Giuliano's async profiler 2024-10-24 16:10:07 +02:00
Ben 5c65d1d74e Fixes message length issue for admin-channel messages. 2024-10-22 13:55:52 +02:00
Ben 292b4b9b06 bumps to 0.1.7 2024-10-22 12:57:07 +02:00
Ben ddbe5b111a Merge branch 'feature/block-retransmit' 2024-10-22 12:54:33 +02:00
Ben a0abea4432 Merge branch 'master' into feature/block-retransmit
# Conflicts:
#	ProjectPlugins/CodexPlugin/ApiChecker.cs
2024-10-22 12:54:22 +02:00
Ben 2f39327db2 Adds multiswarm test 2024-10-22 10:19:00 +02:00
Ben d452293cac makes autoclient delete old data files 2024-10-21 11:10:39 +02:00
benbierens 3e245b707c Fix updating of moment buckets 2024-10-21 11:05:48 +02:00
benbierens 9e842207ab improves error reporting for discord bot 2024-10-18 11:03:05 +02:00
benbierens e29ffe4f9c blockTimeFinder now uses inclusive time ranges 2024-10-18 10:35:49 +02:00
benbierens d8a6df5845 attempt to fix infinite loop in moment-reference-builder 2024-10-18 09:03:19 +02:00
benbierens 1859994ec6 nice csv outputs 2024-10-18 08:45:54 +02:00
Ben Bierens 60b489ced1 Merge pull request #104 from codex-storage/feature/bot-checks-cids
Feature/bot checks cids
2024-10-18 08:23:58 +02:00
benbierens b72b4a850b writes csv 2024-10-17 09:37:15 +02:00
benbierens fc942b11f8 Implements /check command 2024-10-16 14:05:02 +02:00
benbierens 2c88ddfb6b brings in api with new calls 2024-10-16 12:42:52 +02:00
benbierens e352e5c65c Moves code to file 2024-10-14 11:30:07 +02:00
benbierens 2cbe030cff Implements logical operations for indexSets 2024-10-14 11:19:40 +02:00
benbierens a38e93a607 set up logical operations for indexset 2024-10-14 10:10:18 +02:00
benbierens 269365e101 Orders chain events by blocknumber 2024-10-14 09:43:05 +02:00
benbierens 02ca9db001 wip: blockreceives to csv 2024-10-14 09:11:29 +02:00
benbierens 672092b232 handles codex events without node or peer 2024-10-14 09:11:09 +02:00
benbierens 1bd84a4892 combinatorial swarm tests 2024-10-11 11:20:59 +02:00
benbierens 5896735884 set up swarm test 2024-10-11 10:30:07 +02:00
Ben bebeb3766e Fixes unstable transcript test. 2024-10-10 15:31:38 +02:00
Ben e0cbf8c84d Updates emoji maps 2024-10-10 14:03:42 +02:00
Ben 3ed91d7310 Bumps containers to dotnet 8.0 2024-10-10 11:54:42 +02:00
Ben 9573814574 Adds emoji icons to chain events log 2024-10-10 11:45:04 +02:00
Ben 5313f8a7ac Adds SlotReservationsFull event to chain state tracker 2024-10-10 11:15:07 +02:00
Ben c239d555dc More reliable test 2024-10-09 11:37:16 +02:00
Ben 2e9d7641a3 Adds test to capture upload-interrupt node crash. 2024-10-09 09:11:04 +02:00
Ben fc9249da20 updates to 0.1.6 2024-10-08 14:49:19 +02:00
Ben 978e085219 updates contracts plugin 2024-10-08 10:38:49 +02:00
Ben 960b0c3788 Automatic updating of CodexPlugin openapi.yaml. 2024-10-08 09:29:26 +02:00
benbierens 9ca4bf8afc Updates marketplace plugin 2024-10-07 15:59:05 +02:00
Slava cfdc25335c Add .gitattributes for line endings in .bat, .cmd and .sh scripts (#103) 2024-10-05 23:08:49 +03:00
benbierens 04f087efe4 Bump to dotnet 8 2024-10-03 14:02:28 +02:00
Ben f6aa122245 Fixes issue where timesegments is locked up for max 50x segmentDuration when catching up to realtime. 2024-09-30 16:23:55 +02:00
Ben db4c4a87e0 Rewarderbot: Speeds up time segments when catching up to realtime. 2024-09-30 15:34:40 +02:00
Ben ffb5eb294a Fix kubernetes port name issue 2024-09-30 15:23:14 +02:00
Ben 6a8c74e02a Adds short-format UTC to block header for each chain-events channel entry. 2024-09-30 11:09:44 +02:00
Ben 200de1d7f7 Update: Bot reports interactions in admin channel 2024-09-30 10:59:22 +02:00
Ben f801cb082e update 2024-09-24 15:21:33 +02:00
Ben 2a61dad556 updates codex to 0.1.4 2024-09-24 13:06:00 +02:00
Ben 2d90349b7b Updates autobootstrap fixture for parallel running. 2024-09-24 10:10:59 +02:00
Ben c4b6d01530 Adds check for usage of availabilities. 2024-09-23 11:31:57 +02:00
Ben c9fedac592 Adds call to get availabilities 2024-09-23 10:52:12 +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
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
373 changed files with 15414 additions and 3344 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/**
+10
View File
@@ -0,0 +1,10 @@
# Set default behavior to automatically normalize line endings.
* text=auto
# Force bash scripts to always use lf line endings so that if a repo is accessed
# in Unix via a file share from Windows, the scripts will work.
*.sh text eol=lf
# Likewise, force cmd and batch scripts to always use crlf
*.cmd text eol=crlf
*.bat text eol=crlf
+27
View File
@@ -0,0 +1,27 @@
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
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: codex-storage/github-actions/.github/workflows/docker-reusable.yml@master
with:
docker_file: Tools/AutoClient/docker/Dockerfile
dockerhub_repo: codexstorage/codex-autoclient
tag_latest: ${{ github.ref_name == github.event.repository.default_branch || startsWith(github.ref, 'refs/tags/') }}
secrets: inherit
+3 -4
View File
@@ -13,16 +13,15 @@ on:
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-discordbot.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
uses: codex-storage/github-actions/.github/workflows/docker-reusable.yml@master
with:
docker_file: Tools/BiblioTech/docker/Dockerfile
docker_repo: codexstorage/codex-discordbot
dockerhub_repo: codexstorage/codex-discordbot
tag_latest: ${{ github.ref_name == github.event.repository.default_branch || startsWith(github.ref, 'refs/tags/') }}
secrets: inherit
+4 -5
View File
@@ -11,17 +11,16 @@ on:
- 'Tools/KeyMaker/**'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-KeyMaker.yml
- .github/workflows/docker-reusable.yml
- .github/workflows/docker-keymaker.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
uses: codex-storage/github-actions/.github/workflows/docker-reusable.yml@master
with:
docker_file: Tools/KeyMaker/docker/Dockerfile
docker_repo: codexstorage/codex-keymaker
dockerhub_repo: codexstorage/codex-keymaker
tag_latest: ${{ github.ref_name == github.event.repository.default_branch || startsWith(github.ref, 'refs/tags/') }}
secrets: inherit
@@ -0,0 +1,26 @@
name: Docker - MarketInsights API
on:
push:
branches:
- master
tags:
- 'v*.*.*'
paths:
- 'Tools/MarketInsights/**'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-marketinsights.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: codex-storage/github-actions/.github/workflows/docker-reusable.yml@master
with:
docker_file: Tools/MarketInsights/Dockerfile
dockerhub_repo: codexstorage/codex-marketinsights
tag_latest: ${{ github.ref_name == github.event.repository.default_branch || startsWith(github.ref, 'refs/tags/') }}
secrets: inherit
-178
View File
@@ -1,178 +0,0 @@
name: Reusable - Docker
on:
workflow_call:
inputs:
docker_file:
default: docker/Dockerfile
description: Dockerfile
required: false
type: string
docker_repo:
default: codexstorage/cs-codex-dist-tests
description: DockerHub repository
required: false
type: string
tag_latest:
default: true
description: Set latest tag for Docker images
required: false
type: boolean
tag_sha:
default: true
description: Set Git short commit as Docker tag
required: false
type: boolean
tag_suffix:
default: ''
description: Suffix for Docker images tag
required: false
type: string
env:
DOCKER_FILE: ${{ inputs.docker_file }}
DOCKER_REPO: ${{ inputs.docker_repo }}
TAG_LATEST: ${{ inputs.tag_latest }}
TAG_SHA: ${{ inputs.tag_sha }}
TAG_SUFFIX: ${{ inputs.tag_suffix }}
jobs:
# Build platform specific image
build:
strategy:
fail-fast: true
matrix:
target:
- os: linux
arch: amd64
- os: linux
arch: arm64
include:
- target:
os: linux
arch: amd64
builder: ubuntu-22.04
- target:
os: linux
arch: arm64
builder: buildjet-4vcpu-ubuntu-2204-arm
name: Build ${{ matrix.target.os }}/${{ matrix.target.arch }}
runs-on: ${{ matrix.builder }}
env:
PLATFORM: ${{ format('{0}/{1}', 'linux', matrix.target.arch) }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker - Meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_REPO }}
- name: Docker - Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Docker - Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker - Build and Push by digest
id: build
uses: docker/build-push-action@v5
with:
context: .
file: ${{ env.DOCKER_FILE }}
platforms: ${{ env.PLATFORM }}
push: true
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
- name: Docker - Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Docker - Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.target.arch }}
path: /tmp/digests
if-no-files-found: error
retention-days: 1
# Publish multi-platform image
publish:
name: Publish multi-platform image
runs-on: ubuntu-latest
needs: build
steps:
- name: Docker - Variables
run: |
# Adjust custom suffix when set and
if [[ -n "${{ env.TAG_SUFFIX }}" ]]; then
echo "TAG_SUFFIX=-${{ env.TAG_SUFFIX }}" >>$GITHUB_ENV
fi
# Disable SHA tags on tagged release
if [[ ${{ startsWith(github.ref, 'refs/tags/') }} == "true" ]]; then
echo "TAG_SHA=false" >>$GITHUB_ENV
fi
# Handle latest and latest-custom using raw
if [[ ${{ env.TAG_SHA }} == "false" ]]; then
echo "TAG_LATEST=false" >>$GITHUB_ENV
echo "TAG_RAW=true" >>$GITHUB_ENV
if [[ -z "${{ env.TAG_SUFFIX }}" ]]; then
echo "TAG_RAW_VALUE=latest" >>$GITHUB_ENV
else
echo "TAG_RAW_VALUE=latest-{{ env.TAG_SUFFIX }}" >>$GITHUB_ENV
fi
else
echo "TAG_RAW=false" >>$GITHUB_ENV
fi
- name: Docker - Download digests
uses: actions/download-artifact@v4
with:
pattern: digests-*
merge-multiple: true
path: /tmp/digests
- name: Docker - Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Docker - Meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_REPO }}
flavor: |
latest=${{ env.TAG_LATEST }}
suffix=${{ env.TAG_SUFFIX }},onlatest=true
tags: |
type=semver,pattern={{version}}
type=raw,enable=${{ env.TAG_RAW }},value=latest
type=sha,enable=${{ env.TAG_SHA }}
- name: Docker - Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker - Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
- name: Docker - Inspect image
run: |
docker buildx imagetools inspect ${{ env.DOCKER_REPO }}:${{ steps.meta.outputs.version }}
+5 -4
View File
@@ -1,5 +1,6 @@
name: Docker - Rewarder Bot
on:
push:
branches:
@@ -12,15 +13,15 @@ on:
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-rewarder.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
uses: codex-storage/github-actions/.github/workflows/docker-reusable.yml@master
with:
docker_file: Tools/TestNetRewarder/docker/Dockerfile
docker_repo: codexstorage/codex-rewarderbot
dockerhub_repo: codexstorage/codex-rewarderbot
tag_latest: ${{ github.ref_name == github.event.repository.default_branch || startsWith(github.ref, 'refs/tags/') }}
secrets: inherit
+5 -2
View File
@@ -11,12 +11,15 @@ on:
- docker/Dockerfile
- docker/docker-entrypoint.sh
- .github/workflows/docker-runner.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
uses: codex-storage/github-actions/.github/workflows/docker-reusable.yml@master
with:
docker_file: docker/Dockerfile
dockerhub_repo: codexstorage/cs-codex-dist-tests
tag_latest: ${{ github.ref_name == github.event.repository.default_branch || startsWith(github.ref, 'refs/tags/') }}
secrets: inherit
+2 -2
View File
@@ -80,7 +80,7 @@ env:
TESTS_TARGET_DURATION: 2d
TESTS_FILTER: ""
TESTS_CLEANUP: true
JOB_MANIFEST: docker/continuous-tests-job.yaml
JOB_MANIFEST: docker/job-continuous-tests.yaml
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
KUBE_VERSION: v1.28.2
@@ -146,7 +146,7 @@ jobs:
if: false
run: |
# Variables
# We need more than 300 seconds because Auto Scaler may take 3 minutes to tun a node
# We need more than 300 seconds because Auto Scaler may take 3 minutes to run a node
duration=600
namespace="${{ env.NAMESPACE }}"
pod=$(kubectl get pod --selector job-name=${{ env.NAMEPREFIX }} -o jsonpath="{.items[0].metadata.name}")
+1 -1
View File
@@ -32,7 +32,7 @@ env:
NAMEPREFIX: d-tests-runner
NAMESPACE: default
COMMAND: dotnet test Tests/CodexTests
JOB_MANIFEST: docker/dist-tests-job.yaml
JOB_MANIFEST: docker/job-dist-tests.yaml
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
KUBE_VERSION: v1.28.2
+122
View File
@@ -0,0 +1,122 @@
name: Run Release Tests
on:
workflow_dispatch:
inputs:
codexdockerimage:
description: "Codex Docker image (example: 'codexstorage/nim-codex:0.1.8-dist-tests')"
required: true
type: string
workflow_call:
inputs:
source:
description: Repository with tests (current)
required: false
type: string
branch:
description: Branch with tests (master)
required: false
type: string
codexdockerimage:
description: "Codex Docker image (example: 'codexstorage/nim-codex:0.1.8-dist-tests')"
required: true
type: string
workflow_source:
description: Workflow source
required: false
type: string
default: ''
env:
SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }}
BRANCH: ${{ github.ref_name }}
CODEXDOCKERIMAGE: codexstorage/nim-codex:latest-dist-tests
TEST_TYPE: release-tests
NAMEPREFIX: r-tests
NAMESPACE: default
JOB_MANIFEST: docker/job-release-tests.yaml
COMMAND: dotnet test Tests/CodexReleaseTests
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
KUBE_VERSION: v1.30.5
jobs:
run_tests:
name: Run Release Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
repository: ${{ inputs.workflow_source }}
- name: Variables
run: |
RUNID=$(date +%Y%m%d-%H%M%S)
echo "RUNID=${RUNID}" >> $GITHUB_ENV
echo "TESTID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
[[ -n "${{ inputs.source }}" ]] && echo "SOURCE=${{ inputs.source }}" >>"$GITHUB_ENV" || echo "SOURCE=${{ env.SOURCE }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.branch }}" ]] && echo "BRANCH=${{ inputs.branch }}" >>"$GITHUB_ENV" || echo "BRANCH=${{ env.BRANCH }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.codexdockerimage }}" ]] && echo "CODEXDOCKERIMAGE=${{ inputs.codexdockerimage }}" >>"$GITHUB_ENV" || echo "CODEXDOCKERIMAGE=${{ env.CODEXDOCKERIMAGE }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.nameprefix }}" ]] && NAMEPREFIX="`awk '{ print tolower($0) }' <<< ${{ inputs.nameprefix }}`" || NAMEPREFIX="`awk '{ print tolower($0) }' <<< ${{ env.NAMEPREFIX }}`"
echo "NAMEPREFIX=${NAMEPREFIX}-${RUNID}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.namespace }}" ]] && echo "NAMESPACE=${{ inputs.namespace }}" >>"$GITHUB_ENV" || echo "NAMESPACE=${{ env.NAMESPACE }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.command }}" ]] && COMMAND="${{ inputs.command }}" || COMMAND="${{ env.COMMAND }}"
echo "COMMAND=$(jq -c 'split(" ")' <<< '"'${COMMAND}'"')" >>"$GITHUB_ENV"
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
uses: azure/setup-kubectl@v4
with:
version: ${{ env.KUBE_VERSION }}
- name: Kubectl - Kubeconfig
run: |
mkdir -p "${HOME}"/.kube
echo "${{ env.KUBE_CONFIG }}" | base64 -d > "${HOME}"/.kube/config
- name: Kubectl - Create Job to run tests
run: |
envsubst < ${{ env.JOB_MANIFEST }} | kubectl apply -f -
- name: Tests Identification
run: |
echo "----"
echo "Repository: ${{ env.SOURCE }}"
echo "Branch: ${{ env.BRANCH }}"
echo "Runner job: ${{ env.NAMEPREFIX }}"
echo "Runner pod: $(kubectl get pod --selector job-name=${{ env.NAMEPREFIX }} -ojsonpath='{.items[0].metadata.name}')"
echo "Runner namespace: ${{ env.NAMESPACE }}"
echo "----"
- name: Show Runner logs
run: |
# Variables
# We need more than 300 seconds because Auto Scaler may take 3 minutes to tun a node
namespace="${{ env.NAMESPACE }}"
pod=$(kubectl get pod --selector job-name=${{ env.NAMEPREFIX }} -o jsonpath='{.items[0].metadata.name}')
# Check Pod status
WAIT=120
SECONDS=0
sleep=1
while (( SECONDS < WAIT )); do
phase=$(kubectl get pod ${pod} -n ${namespace} -o jsonpath='{.status.phase}')
[[ "${phase}" == "Running" ]] && { echo "Pod $pod is in the $phase state - Get the logs"; break; } || { echo "Pod $pod is in the $phase state - Retry in $sleep second(s) / $((WAIT - SECONDS))"; }
sleep $sleep
done
# Get logs
while [[ $(kubectl get pod ${pod} -n ${namespace} -o jsonpath='{.status.phase}') == "Running" ]]; do
echo "Show ${pod} logs ..."
echo "----"
kubectl logs $pod -n $namespace -f || true
sleep 1
done
- name: Set workflow status from job status
run: |
sleep 5
job_status=$(kubectl get jobs ${{ env.NAMEPREFIX }} -n ${{ env.NAMESPACE }} -o jsonpath='{.status.conditions[0].type}')
echo "Job status: $job_status"
if [[ "${job_status}" != "Complete" ]]; then exit 1; fi
+3 -1
View File
@@ -1,4 +1,6 @@
.vs
obj
bin
.vscode
.vscode
Tools/AutoClient/datapath
.editorconfig
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -1,4 +1,4 @@
namespace NethereumWorkflow.BlockUtils
namespace BlockchainUtils
{
public class BlockCache
{
@@ -1,4 +1,4 @@
namespace NethereumWorkflow.BlockUtils
namespace BlockchainUtils
{
public class BlockTimeEntry
{
@@ -1,6 +1,6 @@
using Logging;
namespace NethereumWorkflow.BlockUtils
namespace BlockchainUtils
{
public class BlockTimeFinder
{
@@ -29,7 +29,8 @@ namespace NethereumWorkflow.BlockUtils
public ulong? GetHighestBlockNumberBefore(DateTime moment)
{
bounds.Initialize();
if (moment <= bounds.Genesis.Utc) return null;
if (moment < bounds.Genesis.Utc) return null;
if (moment == bounds.Genesis.Utc) return bounds.Genesis.BlockNumber;
if (moment >= bounds.Current.Utc) return bounds.Current.BlockNumber;
return Log(() => Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector));
@@ -38,7 +39,8 @@ namespace NethereumWorkflow.BlockUtils
public ulong? GetLowestBlockNumberAfter(DateTime moment)
{
bounds.Initialize();
if (moment >= bounds.Current.Utc) return null;
if (moment > bounds.Current.Utc) return null;
if (moment == bounds.Current.Utc) return bounds.Current.BlockNumber;
if (moment <= bounds.Genesis.Utc) return bounds.Genesis.BlockNumber;
return Log(()=> Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector)); ;
@@ -1,5 +1,11 @@
namespace NethereumWorkflow.BlockUtils
namespace BlockchainUtils
{
public interface IWeb3Blocks
{
ulong GetCurrentBlockNumber();
DateTime? GetTimestampForBlock(ulong blockNumber);
}
public class BlockchainBounds
{
private readonly BlockCache cache;
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nethereum.Web3" Version="4.14.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Logging\Logging.csproj" />
</ItemGroup>
</Project>
@@ -1,7 +1,7 @@
using Nethereum.Hex.HexTypes;
using System.Numerics;
namespace NethereumWorkflow
namespace BlockchainUtils
{
public static class ConversionExtensions
{
+2 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\FileUtils\FileUtils.csproj" />
<ProjectReference Include="..\KubernetesWorkflow\KubernetesWorkflow.csproj" />
<ProjectReference Include="..\WebUtils\WebUtils.csproj" />
</ItemGroup>
</Project>
+2 -5
View File
@@ -1,5 +1,6 @@
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
using Logging;
namespace Core
{
@@ -30,11 +31,7 @@ namespace Core
public IDownloadedLog DownloadLog(RunningContainer container, int? tailLines = null)
{
var workflow = entryPoint.Tools.CreateWorkflow();
var msg = $"Downloading container log for '{container.Name}'";
entryPoint.Tools.GetLog().Log(msg);
var logHandler = new WriteToFileLogHandler(entryPoint.Tools.GetLog(), msg);
workflow.DownloadContainerLog(container, logHandler, tailLines);
return new DownloadedLog(logHandler);
return workflow.DownloadContainerLog(container, tailLines);
}
public string ExecuteContainerCommand(IHasContainer containerSource, string command, params string[] args)
-80
View File
@@ -1,80 +0,0 @@
using KubernetesWorkflow;
using Logging;
namespace Core
{
public interface IDownloadedLog
{
void IterateLines(Action<string> action);
string[] GetLinesContaining(string expectedString);
string[] FindLinesThatContain(params string[] tags);
void DeleteFile();
}
internal class DownloadedLog : IDownloadedLog
{
private readonly LogFile logFile;
internal DownloadedLog(WriteToFileLogHandler logHandler)
{
logFile = logHandler.LogFile;
}
public void IterateLines(Action<string> action)
{
using var file = File.OpenRead(logFile.FullFilename);
using var streamReader = new StreamReader(file);
var line = streamReader.ReadLine();
while (line != null)
{
action(line);
line = streamReader.ReadLine();
}
}
public string[] GetLinesContaining(string expectedString)
{
using var file = File.OpenRead(logFile.FullFilename);
using var streamReader = new StreamReader(file);
var lines = new List<string>();
var line = streamReader.ReadLine();
while (line != null)
{
if (line.Contains(expectedString))
{
lines.Add(line);
}
line = streamReader.ReadLine();
}
return lines.ToArray(); ;
}
public string[] FindLinesThatContain(params string[] tags)
{
var result = new List<string>();
using var file = File.OpenRead(logFile.FullFilename);
using var streamReader = new StreamReader(file);
var line = streamReader.ReadLine();
while (line != null)
{
if (tags.All(line.Contains))
{
result.Add(line);
}
line = streamReader.ReadLine();
}
return result.ToArray();
}
public void DeleteFile()
{
File.Delete(logFile.FullFilename);
}
}
}
+4 -3
View File
@@ -1,5 +1,6 @@
using KubernetesWorkflow;
using Logging;
using WebUtils;
namespace Core
{
@@ -8,16 +9,16 @@ namespace Core
private readonly IToolsFactory toolsFactory;
private readonly PluginManager manager = new PluginManager();
public EntryPoint(ILog log, Configuration configuration, string fileManagerRootFolder, ITimeSet timeSet)
public EntryPoint(ILog log, Configuration configuration, string fileManagerRootFolder, IWebCallTimeSet webCallTimeSet, IK8sTimeSet k8STimeSet)
{
toolsFactory = new ToolsFactory(log, configuration, fileManagerRootFolder, timeSet);
toolsFactory = new ToolsFactory(log, configuration, fileManagerRootFolder, webCallTimeSet, k8STimeSet);
Tools = toolsFactory.CreateTools();
manager.InstantiatePlugins(PluginFinder.GetPluginTypes(), toolsFactory);
}
public EntryPoint(ILog log, Configuration configuration, string fileManagerRootFolder)
: this(log, configuration, fileManagerRootFolder, new DefaultTimeSet())
: this(log, configuration, fileManagerRootFolder, new DefaultWebCallTimeSet(), new DefaultK8sTimeSet())
{
}
+17 -18
View File
@@ -1,12 +1,14 @@
using FileUtils;
using KubernetesWorkflow;
using Logging;
using WebUtils;
namespace Core
{
public interface IPluginTools : IWorkflowTool, ILogTool, IHttpFactoryTool, IFileTool
public interface IPluginTools : IWorkflowTool, ILogTool, IHttpFactory, IFileTool
{
ITimeSet TimeSet { get; }
IWebCallTimeSet WebCallTimeSet { get; }
IK8sTimeSet K8STimeSet { get; }
/// <summary>
/// Deletes kubernetes and tracked file resources.
@@ -25,13 +27,6 @@ namespace Core
ILog GetLog();
}
public interface IHttpFactoryTool
{
IHttp CreateHttp(Action<HttpClient> onClientCreated);
IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet timeSet);
IHttp CreateHttp();
}
public interface IFileTool
{
IFileManager GetFileManager();
@@ -40,37 +35,41 @@ namespace Core
internal class PluginTools : IPluginTools
{
private readonly WorkflowCreator workflowCreator;
private readonly HttpFactory httpFactory;
private readonly IFileManager fileManager;
private readonly LogPrefixer log;
internal PluginTools(ILog log, WorkflowCreator workflowCreator, string fileManagerRootFolder, ITimeSet timeSet)
internal PluginTools(ILog log, WorkflowCreator workflowCreator, string fileManagerRootFolder, IWebCallTimeSet webCallTimeSet, IK8sTimeSet k8STimeSet)
{
this.log = new LogPrefixer(log);
this.workflowCreator = workflowCreator;
TimeSet = timeSet;
httpFactory = new HttpFactory(log, webCallTimeSet);
WebCallTimeSet = webCallTimeSet;
K8STimeSet = k8STimeSet;
fileManager = new FileManager(log, fileManagerRootFolder);
}
public ITimeSet TimeSet { get; }
public IWebCallTimeSet WebCallTimeSet { get; }
public IK8sTimeSet K8STimeSet { 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 httpFactory.CreateHttp(id, onClientCreated);
}
public IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet ts)
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated, IWebCallTimeSet timeSet)
{
return new Http(log, ts, onClientCreated);
return httpFactory.CreateHttp(id, onClientCreated, timeSet);
}
public IHttp CreateHttp()
public IHttp CreateHttp(string id)
{
return new Http(log, TimeSet);
return httpFactory.CreateHttp(id);
}
public IStartupWorkflow CreateWorkflow(string? namespaceOverride = null)
+7 -4
View File
@@ -1,5 +1,6 @@
using KubernetesWorkflow;
using Logging;
using WebUtils;
namespace Core
{
@@ -13,19 +14,21 @@ namespace Core
private readonly ILog log;
private readonly WorkflowCreator workflowCreator;
private readonly string fileManagerRootFolder;
private readonly ITimeSet timeSet;
private readonly IWebCallTimeSet webCallTimeSet;
private readonly IK8sTimeSet k8STimeSet;
public ToolsFactory(ILog log, Configuration configuration, string fileManagerRootFolder, ITimeSet timeSet)
public ToolsFactory(ILog log, Configuration configuration, string fileManagerRootFolder, IWebCallTimeSet webCallTimeSet, IK8sTimeSet k8STimeSet)
{
this.log = log;
workflowCreator = new WorkflowCreator(log, configuration);
this.fileManagerRootFolder = fileManagerRootFolder;
this.timeSet = timeSet;
this.webCallTimeSet = webCallTimeSet;
this.k8STimeSet = k8STimeSet;
}
public PluginTools CreateTools()
{
return new PluginTools(log, workflowCreator, fileManagerRootFolder, timeSet);
return new PluginTools(log, workflowCreator, fileManagerRootFolder, webCallTimeSet, k8STimeSet);
}
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+6 -11
View File
@@ -3,12 +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 ChainEventMessage[] EventsOverview { get; set; } = Array.Empty<ChainEventMessage>();
public string[] Errors { get; set; } = Array.Empty<string>();
public bool HasAny()
{
return Rewards.Any() || Averages.Any() || EventsOverview.Any();
return Rewards.Any() || EventsOverview.Any();
}
}
@@ -18,14 +18,9 @@
public string[] UserAddresses { get; set; } = Array.Empty<string>();
}
public class MarketAverage
public class ChainEventMessage
{
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; }
public ulong BlockNumber { get; set; }
public string Message { get; set; } = string.Empty;
}
}
+51 -46
View File
@@ -7,27 +7,35 @@ 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 const int ChunkSize = 1024 * 1024 * 100;
public FileManager(ILog log, string rootFolder)
{
folder = Path.Combine(rootFolder, folderNumberSource.GetNextNumber().ToString("D5"));
this.log = log;
this.rootFolder = rootFolder;
}
public TrackedFile CreateEmptyFile(string label = "")
@@ -41,10 +49,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 +70,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 +113,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 +149,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);
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+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;
}
}
}
+6
View File
@@ -14,6 +14,12 @@ namespace FileUtils
Label = label;
}
public static TrackedFile FromPath(ILog log, string filepath)
{
// todo: I don't wanne have to do this to call upload.
return new TrackedFile(log, filepath, string.Empty);
}
public string Filename { get; }
public string Label { get; }
+15 -2
View File
@@ -1,4 +1,6 @@
using CodexContractsPlugin;
using BlockchainUtils;
using CodexContractsPlugin;
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
@@ -18,18 +20,29 @@ namespace GethConnector
return null;
}
var gethNode = new CustomGethNode(log, new BlockCache(), GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey);
var config = GetCodexMarketplaceConfig(gethNode, GethInput.MarketplaceAddress);
var contractsDeployment = new CodexContractsDeployment(
config: config,
marketplaceAddress: GethInput.MarketplaceAddress,
abi: GethInput.ABI,
tokenAddress: GethInput.TokenAddress
);
var gethNode = new CustomGethNode(log, GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey);
var contracts = new CodexContractsAccess(log, gethNode, contractsDeployment);
return new GethConnector(gethNode, contracts);
}
private static MarketplaceConfig GetCodexMarketplaceConfig(IGethNode gethNode, string marketplaceAddress)
{
var func = new ConfigurationFunctionBase();
var response = gethNode.Call<ConfigurationFunctionBase, ConfigurationOutputDTO>(marketplaceAddress, func);
return response.ReturnValue1;
}
private GethConnector(IGethNode gethNode, ICodexContracts codexContracts)
{
GethNode = gethNode;
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+6 -2
View File
@@ -45,8 +45,12 @@
private static string? GetEnvVar(List<string> error, string name)
{
var result = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set.");
return result;
if (string.IsNullOrEmpty(result))
{
error.Add($"'{name}' is not set.");
return null;
}
return result.Trim();
}
}
}
@@ -3,7 +3,7 @@ using Logging;
namespace KubernetesWorkflow
{
public class CrashWatcher
public class ContainerCrashWatcher
{
private readonly ILog log;
private readonly KubernetesClientConfiguration config;
@@ -15,7 +15,7 @@ namespace KubernetesWorkflow
private Task? worker;
private Exception? workerException;
public CrashWatcher(ILog log, KubernetesClientConfiguration config, string containerName, string podName, string recipeName, string k8sNamespace)
public ContainerCrashWatcher(ILog log, KubernetesClientConfiguration config, string containerName, string podName, string recipeName, string k8sNamespace)
{
this.log = log;
this.config = config;
@@ -45,7 +45,7 @@ namespace KubernetesWorkflow
if (workerException != null) throw new Exception("Exception occurred in CrashWatcher worker thread.", workerException);
}
public bool HasContainerCrashed()
public bool HasCrashed()
{
using var client = new Kubernetes(config);
var result = HasContainerBeenRestarted(client);
@@ -70,13 +70,13 @@ namespace KubernetesWorkflow
using var client = new Kubernetes(config);
while (!token.IsCancellationRequested)
{
token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
if (HasContainerBeenRestarted(client))
{
DownloadCrashedContainerLogs(client);
return;
}
token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
}
}
@@ -91,7 +91,7 @@ namespace KubernetesWorkflow
private void DownloadCrashedContainerLogs(Kubernetes client)
{
using var stream = client.ReadNamespacedPodLog(podName, k8sNamespace, recipeName, previous: true);
var handler = new WriteToFileLogHandler(log, "Crash detected for " + containerName);
var handler = new WriteToFileLogHandler(log, "Crash detected for " + containerName, containerName);
handler.Log(stream);
}
}
+17 -4
View File
@@ -122,6 +122,16 @@ namespace KubernetesWorkflow
var all = client.Run(c => c.ListNamespace().Items);
var namespaces = all.Select(n => n.Name()).Where(n => n.StartsWith(prefix));
if (wait)
{
// If we're going to wait, trigger the delete for all the namespaces immediately.
// Then wait for them to finish one by one.
foreach (var ns in namespaces)
{
DeleteNamespace(ns, false);
}
}
foreach (var ns in namespaces)
{
DeleteNamespace(ns, wait);
@@ -735,7 +745,10 @@ namespace KubernetesWorkflow
throw new Exception($"Expected to find 1 pod by podLabel '{deployment.PodLabel}'. Found: {pods.Length}. " +
$"Total number of pods: {allPods.Items.Count}. Their labels: {string.Join(Environment.NewLine, allLabels)}");
}
return pods[0];
var pod = pods[0];
if (pod.Status == null) throw new Exception("Pod status unknown");
if (string.IsNullOrEmpty(pod.Status.PodIP)) throw new Exception("Pod IP unknown");
return pod;
}
#endregion
@@ -906,7 +919,7 @@ namespace KubernetesWorkflow
var msg = $"Pod crash detected for deployment {deploymentName} (pod:{podName})";
log.Error(msg);
DownloadPodLog(container, new WriteToFileLogHandler(log, msg), tailLines: null, previous: true);
DownloadPodLog(container, new WriteToFileLogHandler(log, msg, deploymentName), tailLines: null, previous: true);
throw new Exception(msg);
}
@@ -946,13 +959,13 @@ namespace KubernetesWorkflow
#endregion
public CrashWatcher CreateCrashWatcher(RunningContainer container)
public ContainerCrashWatcher CreateCrashWatcher(RunningContainer container)
{
var containerName = container.Name;
var podName = GetPodName(container);
var recipeName = container.Recipe.Name;
return new CrashWatcher(log, cluster.GetK8sClientConfig(), containerName, podName, recipeName, K8sNamespace);
return new ContainerCrashWatcher(log, cluster.GetK8sClientConfig(), containerName, podName, recipeName, K8sNamespace);
}
private V1Pod[] FindPodsByLabel(string podLabel)
+1 -1
View File
@@ -24,8 +24,8 @@
.Replace("]", "-")
.Replace(",", "-");
result = result.Trim('-');
if (result.Length > maxLength) result = result.Substring(0, maxLength);
result = result.Trim('-');
return result;
}
@@ -0,0 +1,42 @@
namespace Core
{
public interface IK8sTimeSet
{
/// <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();
}
public class DefaultK8sTimeSet : IK8sTimeSet
{
public TimeSpan K8sOperationRetryDelay()
{
return TimeSpan.FromSeconds(10);
}
public TimeSpan K8sOperationTimeout()
{
return TimeSpan.FromMinutes(30);
}
}
public class LongK8sTimeSet : IK8sTimeSet
{
public TimeSpan K8sOperationRetryDelay()
{
return TimeSpan.FromSeconds(30);
}
public TimeSpan K8sOperationTimeout()
{
return TimeSpan.FromHours(1);
}
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>KubernetesWorkflow</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
+3 -3
View File
@@ -25,11 +25,11 @@ namespace KubernetesWorkflow
public class WriteToFileLogHandler : LogHandler, ILogHandler
{
public WriteToFileLogHandler(ILog sourceLog, string description)
public WriteToFileLogHandler(ILog sourceLog, string description, string addFileName)
{
LogFile = sourceLog.CreateSubfile();
LogFile = sourceLog.CreateSubfile(addFileName);
var msg = $"{description} -->> {LogFile.FullFilename}";
var msg = $"{description} -->> {LogFile.Filename}";
sourceLog.Log(msg);
LogFile.Write(msg);
@@ -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(),
@@ -13,9 +13,10 @@ namespace KubernetesWorkflow
FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
PodInfo GetPodInfo(RunningContainer container);
PodInfo GetPodInfo(RunningPod pod);
CrashWatcher CreateCrashWatcher(RunningContainer container);
ContainerCrashWatcher CreateCrashWatcher(RunningContainer container);
void Stop(RunningPod pod, bool waitTillStopped);
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(bool wait);
void DeleteNamespacesStartingWith(string namespacePrefix, bool wait);
@@ -60,7 +61,8 @@ namespace KubernetesWorkflow
var startResult = controller.BringOnline(recipes, location);
var containers = CreateContainers(startResult, recipes, startupConfig);
var rc = new RunningPod(startupConfig, startResult, containers);
var info = GetPodInfo(startResult.Deployment);
var rc = new RunningPod(Guid.NewGuid().ToString(), info, startupConfig, startResult, containers);
cluster.Configuration.Hooks.OnContainersStarted(rc);
if (startResult.ExternalService != null)
@@ -82,9 +84,14 @@ namespace KubernetesWorkflow
});
}
public PodInfo GetPodInfo(RunningDeployment deployment)
{
return K8s(c => c.GetPodInfo(deployment));
}
public PodInfo GetPodInfo(RunningContainer container)
{
return K8s(c => c.GetPodInfo(container.RunningPod.StartResult.Deployment));
return GetPodInfo(container.RunningPod.StartResult.Deployment);
}
public PodInfo GetPodInfo(RunningPod pod)
@@ -92,18 +99,26 @@ namespace KubernetesWorkflow
return K8s(c => c.GetPodInfo(pod.StartResult.Deployment));
}
public CrashWatcher CreateCrashWatcher(RunningContainer container)
public ContainerCrashWatcher CreateCrashWatcher(RunningContainer container)
{
return K8s(c => c.CreateCrashWatcher(container));
}
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, bool? previous = null)
@@ -114,6 +129,20 @@ namespace KubernetesWorkflow
});
}
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, container.Name);
K8s(controller =>
{
controller.DownloadPodLog(container, logHandler, tailLines, previous);
});
return new DownloadedLog(logHandler.LogFile, container.Name);
}
public string ExecuteCommand(RunningContainer container, string command, params string[] args)
{
return K8s(controller =>
@@ -147,7 +176,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(Guid.NewGuid().ToString(), name, r, addresses);
}).ToArray();
}
@@ -186,6 +215,7 @@ namespace KubernetesWorkflow
var port = startResult.GetExternalServicePorts(recipe, tag);
return new Address(
logName: $"{recipe.Name}:{tag}",
startResult.Cluster.HostAddress,
port.Number);
}
@@ -197,6 +227,7 @@ namespace KubernetesWorkflow
var port = startResult.GetInternalServicePorts(recipe, tag);
return new Address(
logName: $"{serviceName}:{tag}",
$"http://{serviceName}.{namespaceName}.svc.cluster.local",
port.Number);
}
@@ -7,27 +7,29 @@ namespace KubernetesWorkflow.Types
{
public class RunningContainer
{
public RunningContainer(string name, ContainerRecipe recipe, ContainerAddress[] addresses)
public RunningContainer(string id, string name, ContainerRecipe recipe, ContainerAddress[] addresses)
{
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);
var select = SelectAddress(addresses);
log.Debug($"Container '{Name}' selected for tag '{portTag}' address: '{select}'");
return select.Address;
}
@@ -50,5 +52,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,10 @@ namespace KubernetesWorkflow.Types
{
public class RunningPod
{
public RunningPod(StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
public RunningPod(string id, PodInfo podInfo, StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
{
Id = id;
PodInfo = podInfo;
StartupConfig = startupConfig;
StartResult = startResult;
Containers = containers;
@@ -13,6 +15,8 @@ namespace KubernetesWorkflow.Types
foreach (var c in containers) c.RunningPod = this;
}
public string Id { get; }
public PodInfo PodInfo { get; }
public StartupConfig StartupConfig { get; }
public StartResult StartResult { get; }
public RunningContainer[] Containers { get; }
@@ -23,10 +27,30 @@ namespace KubernetesWorkflow.Types
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
+10 -6
View File
@@ -8,7 +8,7 @@ namespace Logging
void Debug(string message = "", int skipFrames = 0);
void Error(string message);
void AddStringReplace(string from, string to);
LogFile CreateSubfile(string ext = "log");
LogFile CreateSubfile(string addName, string ext = "log");
}
public abstract class BaseLog : ILog
@@ -34,7 +34,7 @@ namespace Logging
{
get
{
if (logFile == null) logFile = new LogFile(GetFullName(), "log");
if (logFile == null) logFile = new LogFile(GetFullName() + ".log");
return logFile;
}
}
@@ -69,15 +69,19 @@ namespace Logging
public virtual void Delete()
{
File.Delete(LogFile.FullFilename);
File.Delete(LogFile.Filename);
}
public LogFile CreateSubfile(string ext = "log")
public LogFile CreateSubfile(string addName, string ext = "log")
{
return new LogFile($"{GetFullName()}_{GetSubfileNumber()}", ext);
addName = addName
.Replace("<", "")
.Replace(">", "");
return new LogFile($"{GetFullName()}_{GetSubfileNumber()}_{addName}.{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));
}
}
}
+79
View File
@@ -0,0 +1,79 @@
namespace Logging
{
public interface IDownloadedLog
{
string SourceName { get; }
void IterateLines(Action<string> action);
void IterateLines(Action<string> action, params string[] thatContain);
string[] GetLinesContaining(string expectedString);
string[] FindLinesThatContain(params string[] tags);
string GetFilepath();
void DeleteFile();
}
public class DownloadedLog : IDownloadedLog
{
private readonly LogFile logFile;
public DownloadedLog(string filepath, string sourceName)
{
logFile = new LogFile(filepath);
SourceName = sourceName;
}
public DownloadedLog(LogFile logFile, string sourceName)
{
this.logFile = logFile;
SourceName = sourceName;
}
public string SourceName { get; }
public void IterateLines(Action<string> action)
{
using var file = File.OpenRead(logFile.Filename);
using var streamReader = new StreamReader(file);
var line = streamReader.ReadLine();
while (line != null)
{
action(line);
line = streamReader.ReadLine();
}
}
public void IterateLines(Action<string> action, params string[] thatContain)
{
IterateLines(line =>
{
if (thatContain.All(line.Contains))
{
action(line);
}
});
}
public string[] GetLinesContaining(string expectedString)
{
return FindLinesThatContain([expectedString]);
}
public string[] FindLinesThatContain(params string[] tags)
{
var result = new List<string>();
IterateLines(result.Add, tags);
return result.ToArray();
}
public string GetFilepath()
{
return logFile.Filename;
}
public void DeleteFile()
{
File.Delete(logFile.Filename);
}
}
}
+20 -17
View File
@@ -1,21 +1,19 @@
namespace Logging
using Utils;
namespace Logging
{
public class LogFile
{
private readonly string extension;
private readonly object fileLock = new object();
private string filename;
public LogFile(string filename, string extension)
public LogFile(string filename)
{
this.filename = filename;
this.extension = extension;
FullFilename = filename + "." + extension;
Filename = filename;
EnsurePathExists(filename);
}
public string FullFilename { get; private set; }
public string Filename { get; private set; }
public void Write(string message)
{
@@ -28,7 +26,7 @@
{
lock (fileLock)
{
File.AppendAllLines(FullFilename, new[] { message });
File.AppendAllLines(Filename, new[] { message });
}
}
catch (Exception ex)
@@ -37,19 +35,24 @@
}
}
public void ConcatToFilename(string toAdd)
public void WriteRawMany(IEnumerable<string> lines)
{
var oldFullName = FullFilename;
filename += toAdd;
FullFilename = filename + "." + extension;
File.Move(oldFullName, FullFilename);
try
{
lock (fileLock)
{
File.AppendAllLines(Filename, lines);
}
}
catch (Exception ex)
{
Console.WriteLine("Writing to log has failed: " + ex);
}
}
private static string GetTimestamp()
{
return $"[{DateTime.UtcNow.ToString("o")}]";
return $"[{Time.FormatTimestamp(DateTime.UtcNow)}]";
}
private void EnsurePathExists(string filename)
+2 -2
View File
@@ -18,9 +18,9 @@
public string Prefix { get; set; } = string.Empty;
public LogFile CreateSubfile(string ext = "log")
public LogFile CreateSubfile(string addName, string ext = "log")
{
return backingLog.CreateSubfile(ext);
return backingLog.CreateSubfile(addName, ext);
}
public void Debug(string message = "", int skipFrames = 0)
+2 -2
View File
@@ -14,9 +14,9 @@
OnAll(l => l.AddStringReplace(from, to));
}
public LogFile CreateSubfile(string ext = "log")
public LogFile CreateSubfile(string addName, string ext = "log")
{
return targetLogs.First().CreateSubfile(ext);
return targetLogs.First().CreateSubfile(addName, ext);
}
public void Debug(string message = "", int skipFrames = 0)
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Logging</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -1,25 +1,25 @@
using Logging;
using BlockchainUtils;
using Logging;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Contracts;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.Web3;
using NethereumWorkflow.BlockUtils;
using Utils;
namespace NethereumWorkflow
{
public class NethereumInteraction
{
// BlockCache is a static instance: It stays alive for the duration of the application runtime.
private readonly static BlockCache blockCache = new BlockCache();
private readonly BlockCache blockCache;
private readonly ILog log;
private readonly Web3 web3;
internal NethereumInteraction(ILog log, Web3 web3)
internal NethereumInteraction(ILog log, Web3 web3, BlockCache blockCache)
{
this.log = log;
this.web3 = web3;
this.blockCache = blockCache;
}
public string SendEth(string toAddress, decimal ethAmount)
@@ -50,6 +50,21 @@ namespace NethereumWorkflow
return Time.Wait(handler.QueryAsync<TResult>(contractAddress, function));
}
public TResult Call<TFunction, TResult>(string contractAddress, TFunction function, ulong blockNumber) where TFunction : FunctionMessage, new()
{
log.Debug(typeof(TFunction).ToString());
var handler = web3.Eth.GetContractQueryHandler<TFunction>();
return Time.Wait(handler.QueryAsync<TResult>(contractAddress, function, new BlockParameter(blockNumber)));
}
public void Call<TFunction>(string contractAddress, TFunction function, ulong blockNumber) where TFunction : FunctionMessage, new()
{
log.Debug(typeof(TFunction).ToString());
var handler = web3.Eth.GetContractQueryHandler<TFunction>();
var result = Time.Wait(handler.QueryRawAsync(contractAddress, function, new BlockParameter(blockNumber)));
var aaaa = 0;
}
public string SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
{
log.Debug();
@@ -1,4 +1,5 @@
using Logging;
using BlockchainUtils;
using Logging;
using Nethereum.Web3;
namespace NethereumWorkflow
@@ -6,13 +7,15 @@ namespace NethereumWorkflow
public class NethereumInteractionCreator
{
private readonly ILog log;
private readonly BlockCache blockCache;
private readonly string ip;
private readonly int port;
private readonly string privateKey;
public NethereumInteractionCreator(ILog log, string ip, int port, string privateKey)
public NethereumInteractionCreator(ILog log, BlockCache blockCache, string ip, int port, string privateKey)
{
this.log = log;
this.blockCache = blockCache;
this.ip = ip;
this.port = port;
this.privateKey = privateKey;
@@ -21,7 +24,7 @@ namespace NethereumWorkflow
public NethereumInteraction CreateWorkflow()
{
log.Debug("Starting interaction to " + ip + ":" + port);
return new NethereumInteraction(log, CreateWeb3());
return new NethereumInteraction(log, CreateWeb3(), blockCache);
}
private Web3 CreateWeb3()
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>NethereumWorkflow</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BlockchainUtils\BlockchainUtils.csproj" />
<ProjectReference Include="..\Logging\Logging.csproj" />
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>
+2 -7
View File
@@ -1,16 +1,11 @@
using Logging;
using BlockchainUtils;
using Logging;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.Web3;
using Utils;
namespace NethereumWorkflow
{
public interface IWeb3Blocks
{
ulong GetCurrentBlockNumber();
DateTime? GetTimestampForBlock(ulong blockNumber);
}
public class Web3Wrapper : IWeb3Blocks
{
private readonly Web3 web3;
@@ -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,151 @@
using Logging;
using Newtonsoft.Json;
using System.Collections.Concurrent;
namespace OverwatchTranscript
{
public interface IFinalizedBucket
{
bool IsEmpty { get; }
void Update();
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 readonly AutoResetEvent itemEnqueued = new AutoResetEvent(false);
private bool sourceIsEmpty;
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 void Update()
{
if (IsEmpty) return;
while (topQueue.Count == 0)
{
UpdateIsEmpty();
if (IsEmpty) return;
itemDequeued.Set();
itemEnqueued.WaitOne(200);
}
}
public DateTime? SeeTopUtc()
{
if (IsEmpty) return null;
if (topQueue.TryPeek(out BucketTop? top))
{
return top.Utc;
}
return null;
}
public BucketTop? TakeTop()
{
if (IsEmpty) return null;
if (topQueue.TryDequeue(out BucketTop? top))
{
itemDequeued.Set();
return top;
}
return null;
}
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);
itemEnqueued.Set();
}
else
{
sourceIsEmpty = true;
UpdateIsEmpty();
return;
}
}
itemDequeued.Reset();
itemDequeued.WaitOne(5000);
}
}
private void UpdateIsEmpty()
{
var allEmpty = sourceIsEmpty && topQueue.IsEmpty;
if (!IsEmpty && allEmpty)
{
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,153 @@
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())
{
foreach (var b in buckets) b.Update();
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>net8.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.");
}
}
}
+19 -1
View File
@@ -2,12 +2,14 @@
{
public class Address
{
public Address(string host, int port)
public Address(string logName, string host, int port)
{
LogName = logName;
Host = host;
Port = port;
}
public string LogName { get; }
public string Host { get; }
public int Port { get; }
@@ -20,5 +22,21 @@
{
return !string.IsNullOrEmpty(Host) && Port > 0;
}
public static Address Empty()
{
return new Address(string.Empty, string.Empty, 0);
}
}
public interface IHasMetricsScrapeTarget
{
Address GetMetricsScrapeTarget();
}
public interface IHasManyMetricScrapeTargets
{
Address[] GetMetricsScrapeTargets();
}
}
+64
View File
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace Utils
{
public static class Base58
{
private const string Digits = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
public static string Encode(byte[] data)
{
// Decode byte[] to BigInteger
BigInteger intData = 0;
for (int i = 0; i < data.Length; i++)
{
intData = intData * 256 + data[i];
}
// Encode BigInteger to Base58 string
string result = "";
while (intData > 0)
{
int remainder = (int)(intData % 58);
intData /= 58;
result = Digits[remainder] + result;
}
// Append `1` for each leading 0 byte
for (int i = 0; i < data.Length && data[i] == 0; i++)
{
result = '1' + result;
}
return result;
}
public static byte[] Decode(string s)
{
BigInteger intData = 0;
for (int i = 0; i < s.Length; i++)
{
int digit = Digits.IndexOf(s[i]); //Slow
if (digit < 0)
throw new FormatException(string.Format("Invalid Base58 character `{0}` at position {1}", s[i], i));
intData = intData * 58 + digit;
}
// Encode BigInteger to byte[]
// Leading zero bytes get encoded as leading `1` characters
int leadingZeroCount = s.TakeWhile(c => c == '1').Count();
var leadingZeros = Enumerable.Repeat((byte)0, leadingZeroCount);
var bytesWithoutLeadingZeros =
intData.ToByteArray()
.Reverse()// to big endian
.SkipWhile(b => b == 0);//strip sign byte
var result = leadingZeros.Concat(bytesWithoutLeadingZeros).ToArray();
return result;
}
}
}
+2 -1
View File
@@ -15,12 +15,13 @@
To = from;
}
TimeRange = timeRange;
NumberOfBlocks = (To - From) + 1;
}
public ulong From { get; }
public ulong To { get; }
public TimeRange TimeRange { get; }
public ulong NumberOfBlocks => To - From;
public ulong NumberOfBlocks { get; }
public override string ToString()
{
-1
View File
@@ -13,7 +13,6 @@
public long SizeInBytes { get; }
public long ToMB()
{
return SizeInBytes / (1024 * 1024);
+9
View File
@@ -0,0 +1,9 @@
namespace Utils
{
//public interface ICrashWatcher
//{
// void Start();
// void Stop();
// bool HasCrashed();
//}
}
+20
View File
@@ -0,0 +1,20 @@
namespace Utils
{
[Serializable]
public class EthAccount
{
public EthAccount(EthAddress ethAddress, string privateKey)
{
EthAddress = ethAddress;
PrivateKey = privateKey;
}
public EthAddress EthAddress { get; }
public string PrivateKey { get; }
public override string ToString()
{
return EthAddress.ToString();
}
}
}
@@ -1,4 +1,4 @@
namespace GethPlugin
namespace Utils
{
public interface IHasEthAddress
{
@@ -1,4 +1,4 @@
namespace GethPlugin
namespace Utils
{
public class Ether : IComparable<Ether>
{
@@ -28,7 +28,13 @@
public override string ToString()
{
return $"{Eth} Eth";
var weiOnly = Wei % TokensIntExtensions.WeiPerEth;
var tokens = new List<string>();
if (Eth > 0) tokens.Add($"{Eth} Eth");
if (weiOnly > 0) tokens.Add($"{weiOnly} Wei");
return string.Join(" + ", tokens);
}
}
+41
View File
@@ -0,0 +1,41 @@
namespace Utils
{
public static class PluginPathUtils
{
private const string ProjectPluginsFolderName = "ProjectPlugins";
private static string projectPluginsDir = string.Empty;
public static string ProjectPluginsDir
{
get
{
if (string.IsNullOrEmpty(projectPluginsDir)) projectPluginsDir = FindProjectPluginsDir();
return projectPluginsDir;
}
}
private static string FindProjectPluginsDir()
{
var current = Directory.GetCurrentDirectory();
while (true)
{
var localFolders = Directory.GetDirectories(current);
var projectPluginsFolders = localFolders.Where(l => l.EndsWith(ProjectPluginsFolderName)).ToArray();
if (projectPluginsFolders.Length == 1)
{
return projectPluginsFolders.Single();
}
var parent = Directory.GetParent(current);
if (parent == null)
{
var msg = $"Unable to locate '{ProjectPluginsFolderName}' folder. Travelled up from: '{Directory.GetCurrentDirectory()}'";
Console.WriteLine(msg);
throw new Exception(msg);
}
current = parent.FullName;
}
}
}
}
+32 -4
View File
@@ -3,13 +3,41 @@
public static class RandomUtils
{
private static readonly Random random = new Random();
private static readonly object @lock = new object();
public static T GetOneRandom<T>(this T[] items)
{
lock (@lock)
{
var i = random.Next(0, items.Length);
var result = items[i];
return result;
}
}
public static T PickOneRandom<T>(this List<T> remainingItems)
{
var i = random.Next(0, remainingItems.Count);
var result = remainingItems[i];
remainingItems.RemoveAt(i);
return result;
lock (@lock)
{
var i = random.Next(0, remainingItems.Count);
var result = remainingItems[i];
remainingItems.RemoveAt(i);
return result;
}
}
public static T[] Shuffled<T>(T[] items)
{
lock (@lock)
{
var result = new List<T>();
var source = items.ToList();
while (source.Any())
{
result.Add(RandomUtils.PickOneRandom(source));
}
return result.ToArray();
}
}
}
}
+5 -1
View File
@@ -71,6 +71,10 @@
task();
return;
}
catch (OperationCanceledException)
{
return;
}
catch (Exception ex)
{
var failure = CaptureFailure(ex);
@@ -94,7 +98,7 @@
private void Fail()
{
throw new TimeoutException($"Retry '{description}' timed out after {tryNumber} tries over {Time.FormatDuration(Duration())}: {GetFailureReport}",
throw new TimeoutException($"Retry '{description}' timed out after {tryNumber} tries over {Time.FormatDuration(Duration())}: {GetFailureReport()}",
new AggregateException(failures.Select(f => f.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);
}
}
}
@@ -1,6 +1,6 @@
using System.Numerics;
namespace CodexContractsPlugin
namespace Utils
{
public class TestToken : IComparable<TestToken>
{
@@ -46,6 +46,16 @@ namespace CodexContractsPlugin
return new TestToken(a.TstWei + b.TstWei);
}
public static TestToken operator -(TestToken a, TestToken b)
{
return new TestToken(a.TstWei - b.TstWei);
}
public static TestToken operator *(TestToken a, int b)
{
return new TestToken(a.TstWei * b);
}
public static bool operator <(TestToken a, TestToken b)
{
return a.TstWei < b.TstWei;
@@ -71,7 +81,7 @@ namespace CodexContractsPlugin
{
public static TestToken TstWei(this int i)
{
return TstWei(Convert.ToDecimal(i));
return new TestToken(new BigInteger(i));
}
public static TestToken TstWei(this decimal i)
@@ -84,6 +94,11 @@ namespace CodexContractsPlugin
return new TestToken(i);
}
public static TestToken TstWei(this string s)
{
return new TestToken(BigInteger.Parse(s));
}
public static TestToken Tst(this int i)
{
return Tst(Convert.ToDecimal(i));
@@ -98,5 +113,10 @@ namespace CodexContractsPlugin
{
return new TestToken(i * TestToken.WeiFactor);
}
public static TestToken Tst(this string s)
{
return new TestToken(BigInteger.Parse(s) * TestToken.WeiFactor);
}
}
}
+5
View File
@@ -33,6 +33,11 @@
result += $"{d.Seconds} secs";
return result;
}
public static string FormatTimestamp(DateTime d)
{
return d.ToString("o");
}
public static TimeSpan ParseTimespan(string span)
{
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Utils</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -1,11 +1,10 @@
using Logging;
using Newtonsoft.Json;
using Serialization = Newtonsoft.Json.Serialization;
using System.Net.Http.Headers;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace Core
namespace WebUtils
{
public interface IEndpoint
{
@@ -119,7 +118,7 @@ namespace Core
var errors = new List<string>();
var deserialized = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings()
{
Error = delegate (object? sender, Serialization.ErrorEventArgs args)
Error = delegate (object? sender, Newtonsoft.Json.Serialization.ErrorEventArgs args)
{
if (args.CurrentObject == args.ErrorContext.OriginalObject)
{
@@ -1,7 +1,7 @@
using Logging;
using Utils;
namespace Core
namespace WebUtils
{
public interface IHttp
{
@@ -13,18 +13,21 @@ namespace Core
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 IWebCallTimeSet 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, IWebCallTimeSet timeSet)
: this(id, log, timeSet, DoNothing)
{
}
internal Http(ILog log, ITimeSet timeSet, Action<HttpClient> onClientCreated)
internal Http(string id, ILog log, IWebCallTimeSet timeSet, Action<HttpClient> onClientCreated)
{
this.id = id;
this.log = log;
this.timeSet = timeSet;
this.onClientCreated = onClientCreated;
@@ -63,12 +66,22 @@ namespace Core
private T LockRetry<T>(Func<T> operation, Retry retry)
{
var httpLock = GetLock();
lock (httpLock)
{
return retry.Run(operation);
}
}
private object GetLock()
{
lock (lockLock) // I had to.
{
if (!httpLocks.ContainsKey(id)) httpLocks.Add(id, new object());
return httpLocks[id];
}
}
private HttpClient GetClient()
{
var client = new HttpClient();
+43
View File
@@ -0,0 +1,43 @@
using Logging;
namespace WebUtils
{
public interface IHttpFactory
{
IHttp CreateHttp(string id, Action<HttpClient> onClientCreated);
IHttp CreateHttp(string id, Action<HttpClient> onClientCreated, IWebCallTimeSet timeSet);
IHttp CreateHttp(string id);
}
public class HttpFactory : IHttpFactory
{
private readonly ILog log;
private readonly IWebCallTimeSet defaultTimeSet;
public HttpFactory(ILog log)
: this (log, new DefaultWebCallTimeSet())
{
}
public HttpFactory(ILog log, IWebCallTimeSet defaultTimeSet)
{
this.log = log;
this.defaultTimeSet = defaultTimeSet;
}
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated)
{
return CreateHttp(id, onClientCreated, defaultTimeSet);
}
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated, IWebCallTimeSet ts)
{
return new Http(id, log, ts, onClientCreated);
}
public IHttp CreateHttp(string id)
{
return new Http(id, log, defaultTimeSet);
}
}
}
@@ -1,6 +1,6 @@
namespace Core
namespace WebUtils
{
public interface ITimeSet
public interface IWebCallTimeSet
{
/// <summary>
/// Timeout for a single HTTP call.
@@ -17,48 +17,27 @@
/// 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();
}
public class DefaultTimeSet : ITimeSet
public class DefaultWebCallTimeSet : IWebCallTimeSet
{
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromMinutes(3);
return TimeSpan.FromMinutes(2);
}
public TimeSpan HttpRetryTimeout()
{
return TimeSpan.FromMinutes(10);
return TimeSpan.FromMinutes(5);
}
public TimeSpan HttpCallRetryDelay()
{
return TimeSpan.FromSeconds(1);
}
public TimeSpan K8sOperationRetryDelay()
{
return TimeSpan.FromSeconds(10);
}
public TimeSpan K8sOperationTimeout()
{
return TimeSpan.FromMinutes(30);
}
}
public class LongTimeSet : ITimeSet
public class LongWebCallTimeSet : IWebCallTimeSet
{
public TimeSpan HttpCallTimeout()
{
@@ -74,15 +53,5 @@
{
return TimeSpan.FromSeconds(20);
}
public TimeSpan K8sOperationRetryDelay()
{
return TimeSpan.FromSeconds(30);
}
public TimeSpan K8sOperationTimeout()
{
return TimeSpan.FromHours(1);
}
}
}
+18
View File
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.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" />
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>
</Project>
+314
View File
@@ -0,0 +1,314 @@
using CodexOpenApi;
using Logging;
using Newtonsoft.Json;
using Utils;
using WebUtils;
namespace CodexClient
{
public class CodexAccess
{
private readonly ILog log;
private readonly IHttpFactory httpFactory;
private readonly IProcessControl processControl;
private ICodexInstance instance;
private readonly Mapper mapper = new Mapper();
public CodexAccess(ILog log, IHttpFactory httpFactory, IProcessControl processControl, ICodexInstance instance)
{
this.log = log;
this.httpFactory = httpFactory;
this.processControl = processControl;
this.instance = instance;
}
public void Stop(bool waitTillStopped)
{
processControl.Stop(waitTillStopped);
// Prevents accidental use after stop:
instance = null!;
}
public IDownloadedLog DownloadLog(string additionalName = "")
{
var file = log.CreateSubfile(GetName() + additionalName);
Log($"Downloading logs to '{file.Filename}'");
return processControl.DownloadLog(file);
}
public string GetImageName()
{
return instance.ImageName;
}
public DateTime GetStartUtc()
{
return instance.StartUtc;
}
public DebugInfo GetDebugInfo()
{
return mapper.Map(OnCodex(api => api.GetDebugInfoAsync()));
}
public void SetLogLevel(string logLevel)
{
try
{
OnCodex(async api =>
{
await api.SetDebugLogLevelAsync(logLevel);
return string.Empty;
});
}
catch (Exception exc)
{
log.Error("Failed to set log level: " + exc);
}
}
public string GetSpr()
{
return CrashCheck(() =>
{
var endpoint = GetEndpoint();
var json = endpoint.HttpGetString("spr");
var response = JsonConvert.DeserializeObject<SprResponse>(json);
return response!.Spr;
});
}
private class SprResponse
{
public string Spr { get; set; } = string.Empty;
}
public DebugPeer GetDebugPeer(string peerId)
{
// Cannot use openAPI: debug/peer endpoint is not specified there.
return CrashCheck(() =>
{
var endpoint = GetEndpoint();
var str = endpoint.HttpGetString($"debug/peer/{peerId}");
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)
{
OnCodex(api =>
{
Time.Wait(api.ConnectPeerAsync(peerId, peerMultiAddresses));
return Task.FromResult(string.Empty);
});
}
public string UploadFile(UploadInput uploadInput)
{
return OnCodex(api => api.UploadAsync(uploadInput.ContentType, uploadInput.ContentDisposition, uploadInput.FileStream));
}
public Stream DownloadFile(string contentId)
{
var fileResponse = OnCodex(api => api.DownloadNetworkStreamAsync(contentId));
if (fileResponse.StatusCode != 200) throw new Exception("Download failed with StatusCode: " + fileResponse.StatusCode);
return fileResponse.Stream;
}
public LocalDataset DownloadStreamless(ContentId cid)
{
var response = OnCodex(api => api.DownloadNetworkAsync(cid.Id));
return mapper.Map(response);
}
public LocalDataset DownloadManifestOnly(ContentId cid)
{
var response = OnCodex(api => api.DownloadNetworkManifestAsync(cid.Id));
return mapper.Map(response);
}
public LocalDatasetList LocalFiles()
{
return mapper.Map(OnCodex(api => api.ListDataAsync()));
}
public StorageAvailability SalesAvailability(StorageAvailability request)
{
var body = mapper.Map(request);
var read = OnCodex(api => api.OfferStorageAsync(body));
return mapper.Map(read);
}
public StorageAvailability[] GetAvailabilities()
{
var collection = OnCodex(api => api.GetAvailabilitiesAsync());
return mapper.Map(collection);
}
public string RequestStorage(StoragePurchaseRequest request)
{
var body = mapper.Map(request);
return OnCodex(api => api.CreateStorageRequestAsync(request.ContentId.Id, body));
}
public CodexSpace Space()
{
var space = OnCodex(api => api.SpaceAsync());
return mapper.Map(space);
}
public StoragePurchase? GetPurchaseStatus(string purchaseId)
{
return CrashCheck(() =>
{
var endpoint = GetEndpoint();
try
{
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));
}
catch (Exception exc)
{
log.Error($"Failed to fetch purchase information for id: '{purchaseId}'. Exception: {exc.Message}");
return null;
}
});
// TODO: current getpurchase api does not line up with its openapi spec.
// return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId)));
}
public string GetName()
{
return instance.Name;
}
public Address GetDiscoveryEndpoint()
{
return instance.DiscoveryEndpoint;
}
public Address GetApiEndpoint()
{
return instance.ApiEndpoint;
}
public Address GetListenEndpoint()
{
return instance.ListenEndpoint;
}
public bool HasCrashed()
{
return processControl.HasCrashed();
}
public Address? GetMetricsEndpoint()
{
return instance.MetricsEndpoint;
}
public EthAccount? GetEthAccount()
{
return instance.EthAccount;
}
public void DeleteDataDirFolder()
{
processControl.DeleteDataDirFolder();
}
private T OnCodex<T>(Func<CodexApiClient, Task<T>> action)
{
var result = httpFactory.CreateHttp(GetHttpId(), h => CheckContainerCrashed()).OnClient(client => CallCodex(client, action));
return result;
}
private T OnCodex<T>(Func<CodexApiClient, Task<T>> action, Retry retry)
{
var result = httpFactory.CreateHttp(GetHttpId(), h => CheckContainerCrashed()).OnClient(client => CallCodex(client, action), retry);
return result;
}
private T CallCodex<T>(HttpClient client, Func<CodexApiClient, Task<T>> action)
{
var address = GetAddress();
var api = new CodexApiClient(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
{
CheckContainerCrashed();
}
}
private IEndpoint GetEndpoint()
{
return httpFactory
.CreateHttp(GetHttpId(), h => CheckContainerCrashed())
.CreateEndpoint(GetAddress(), "/api/codex/v1/", GetName());
}
private Address GetAddress()
{
return instance.ApiEndpoint;
}
private string GetHttpId()
{
return GetAddress().ToString();
}
private void CheckContainerCrashed()
{
if (processControl.HasCrashed()) throw new Exception($"Container {GetName()} has crashed.");
}
private void Throw(Failure failure)
{
throw failure.Exception;
}
private void Log(string msg)
{
log.Log($"({GetName()}) {msg}");
}
}
public class UploadInput
{
public UploadInput(string contentType, string contentDisposition, FileStream fileStream)
{
ContentType = contentType;
ContentDisposition = contentDisposition;
FileStream = fileStream;
}
public string ContentType { get; }
public string ContentDisposition { get; }
public FileStream FileStream { get; }
}
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="openapi.yaml" />
</ItemGroup>
<ItemGroup>
<OpenApiReference Include="openapi.yaml" CodeGenerator="NSwagCSharp" Namespace="CodexOpenApi" ClassName="CodexApiClient" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="7.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.18.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\FileUtils\FileUtils.csproj" />
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\..\Framework\WebUtils\WebUtils.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,54 @@
using Utils;
namespace CodexClient
{
public interface ICodexInstance
{
string Name { get; }
string ImageName { get; }
DateTime StartUtc { get; }
Address DiscoveryEndpoint { get; }
Address ApiEndpoint { get; }
Address ListenEndpoint { get; }
EthAccount? EthAccount { get; }
Address? MetricsEndpoint { get; }
}
public class CodexInstance : ICodexInstance
{
public CodexInstance(string name, string imageName, DateTime startUtc, Address discoveryEndpoint, Address apiEndpoint, Address listenEndpoint, EthAccount? ethAccount, Address? metricsEndpoint)
{
Name = name;
ImageName = imageName;
StartUtc = startUtc;
DiscoveryEndpoint = discoveryEndpoint;
ApiEndpoint = apiEndpoint;
ListenEndpoint = listenEndpoint;
EthAccount = ethAccount;
MetricsEndpoint = metricsEndpoint;
}
public string Name { get; }
public string ImageName { get; }
public DateTime StartUtc { get; }
public Address DiscoveryEndpoint { get; }
public Address ApiEndpoint { get; }
public Address ListenEndpoint { get; }
public EthAccount? EthAccount { get; }
public Address? MetricsEndpoint { get; }
public static ICodexInstance CreateFromApiEndpoint(string name, Address apiEndpoint, EthAccount? ethAccount = null)
{
return new CodexInstance(
name,
imageName: "-",
startUtc: DateTime.UtcNow,
discoveryEndpoint: Address.Empty(),
apiEndpoint: apiEndpoint,
listenEndpoint: Address.Empty(),
ethAccount: ethAccount,
metricsEndpoint: null
);
}
}
}
@@ -1,4 +1,4 @@
namespace CodexPlugin
namespace CodexClient
{
public enum CodexLogLevel
{
@@ -1,6 +1,6 @@
using System.Globalization;
namespace CodexPlugin
namespace CodexClient
{
public class CodexLogLine
{
+409
View File
@@ -0,0 +1,409 @@
using CodexClient.Hooks;
using FileUtils;
using Logging;
using Utils;
namespace CodexClient
{
public partial interface ICodexNode : IHasEthAddress, IHasMetricsScrapeTarget
{
string GetName();
string GetImageName();
string GetPeerId();
DebugInfo GetDebugInfo(bool log = false);
void SetLogLevel(string logLevel);
string GetSpr();
DebugPeer GetDebugPeer(string peerId);
ContentId UploadFile(TrackedFile file);
ContentId UploadFile(TrackedFile file, string contentType, string contentDisposition);
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
LocalDataset DownloadStreamless(ContentId cid);
/// <summary>
/// TODO: This will monitor the quota-used of the node until 'size' bytes are added. That's a very bad way
/// to track the streamless download progress. Replace it once we have a good API for this.
/// </summary>
LocalDataset DownloadStreamlessWait(ContentId cid, ByteSize size);
LocalDataset DownloadManifestOnly(ContentId cid);
LocalDatasetList LocalFiles();
CodexSpace Space();
void ConnectToPeer(ICodexNode node);
DebugInfoVersion Version { get; }
IMarketplaceAccess Marketplace { get; }
ITransferSpeeds TransferSpeeds { get; }
EthAccount EthAccount { get; }
StoragePurchase? GetPurchaseStatus(string purchaseId);
Address GetDiscoveryEndpoint();
Address GetApiEndpoint();
Address GetListenEndpoint();
/// <summary>
/// Warning! The node is not usable after this.
/// TODO: Replace with delete-blocks debug call once available in Codex.
/// </summary>
void DeleteDataDirFolder();
void Stop(bool waitTillStopped);
IDownloadedLog DownloadLog(string additionalName = "");
bool HasCrashed();
}
public class CodexNode : ICodexNode
{
private const string UploadFailedMessage = "Unable to store block";
private readonly ILog log;
private readonly ICodexNodeHooks hooks;
private readonly TransferSpeeds transferSpeeds;
private string peerId = string.Empty;
private string nodeId = string.Empty;
private readonly CodexAccess codexAccess;
private readonly IFileManager fileManager;
public CodexNode(ILog log, CodexAccess codexAccess, IFileManager fileManager, IMarketplaceAccess marketplaceAccess, ICodexNodeHooks hooks)
{
this.codexAccess = codexAccess;
this.fileManager = fileManager;
Marketplace = marketplaceAccess;
this.hooks = hooks;
Version = new DebugInfoVersion();
transferSpeeds = new TransferSpeeds();
this.log = new LogPrefixer(log, $"{GetName()} ");
}
public void Awake()
{
hooks.OnNodeStarting(codexAccess.GetStartUtc(), codexAccess.GetImageName(), codexAccess.GetEthAccount());
}
public void Initialize()
{
InitializePeerNodeId();
InitializeLogReplacements();
hooks.OnNodeStarted(this, peerId, nodeId);
}
public IMarketplaceAccess Marketplace { get; }
public DebugInfoVersion Version { get; private set; }
public ITransferSpeeds TransferSpeeds { get => transferSpeeds; }
public StoragePurchase? GetPurchaseStatus(string purchaseId)
{
return codexAccess.GetPurchaseStatus(purchaseId);
}
public EthAddress EthAddress
{
get
{
EnsureMarketplace();
return codexAccess.GetEthAccount()!.EthAddress;
}
}
public EthAccount EthAccount
{
get
{
EnsureMarketplace();
return codexAccess.GetEthAccount()!;
}
}
public string GetName()
{
return codexAccess.GetName();
}
public string GetImageName()
{
return codexAccess.GetImageName();
}
public string GetPeerId()
{
return peerId;
}
public DebugInfo GetDebugInfo(bool log = false)
{
var debugInfo = codexAccess.GetDebugInfo();
if (log)
{
var known = string.Join(",", debugInfo.Table.Nodes.Select(n => n.PeerId));
Log($"Got DebugInfo with id: {debugInfo.Id}. This node knows: [{known}]");
}
return debugInfo;
}
public void SetLogLevel(string logLevel)
{
codexAccess.SetLogLevel(logLevel);
}
public string GetSpr()
{
return codexAccess.GetSpr();
}
public DebugPeer GetDebugPeer(string peerId)
{
return codexAccess.GetDebugPeer(peerId);
}
public ContentId UploadFile(TrackedFile file)
{
return UploadFile(file, "application/octet-stream", $"attachment; filename=\"{Path.GetFileName(file.Filename)}\"");
}
public ContentId UploadFile(TrackedFile file, string contentType, string contentDisposition)
{
using var fileStream = File.OpenRead(file.Filename);
var uniqueId = Guid.NewGuid().ToString();
var size = file.GetFilesize();
hooks.OnFileUploading(uniqueId, size);
var input = new UploadInput(contentType, contentDisposition, fileStream);
var logMessage = $"Uploading file {file.Describe()} with contentType: '{input.ContentType}' and disposition: '{input.ContentDisposition}'...";
var measurement = Stopwatch.Measure(log, logMessage, () =>
{
return codexAccess.UploadFile(input);
});
var response = measurement.Value;
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 {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 file = fileManager.CreateEmptyFile(fileLabel);
hooks.OnFileDownloading(contentId);
Log($"Downloading '{contentId}'...");
var logMessage = $"Downloaded '{contentId}' to '{file.Filename}'";
var measurement = Stopwatch.Measure(log, logMessage, () => DownloadToFile(contentId.Id, file));
var size = file.GetFilesize();
transferSpeeds.AddDownloadSample(size, measurement);
hooks.OnFileDownloaded(size, contentId);
return file;
}
public LocalDataset DownloadStreamless(ContentId cid)
{
Log($"Downloading streamless '{cid}' (no-wait)");
return codexAccess.DownloadStreamless(cid);
}
public LocalDataset DownloadStreamlessWait(ContentId cid, ByteSize size)
{
Log($"Downloading streamless '{cid}' (wait till finished)");
var sw = Stopwatch.Measure(log, nameof(DownloadStreamlessWait), () =>
{
var startSpace = Space();
var result = codexAccess.DownloadStreamless(cid);
WaitUntilQuotaUsedIncreased(startSpace, size);
return result;
});
return sw.Value;
}
public LocalDataset DownloadManifestOnly(ContentId cid)
{
Log($"Downloading manifest-only '{cid}'");
return codexAccess.DownloadManifestOnly(cid);
}
public LocalDatasetList LocalFiles()
{
return codexAccess.LocalFiles();
}
public CodexSpace Space()
{
return codexAccess.Space();
}
public void ConnectToPeer(ICodexNode node)
{
var peer = (CodexNode)node;
Log($"Connecting to peer {peer.GetName()}...");
var peerInfo = node.GetDebugInfo();
codexAccess.ConnectToPeer(peerInfo.Id, GetPeerMultiAddresses(peer, peerInfo));
Log($"Successfully connected to peer {peer.GetName()}.");
}
public void DeleteDataDirFolder()
{
codexAccess.DeleteDataDirFolder();
}
public void Stop(bool waitTillStopped)
{
Log("Stopping...");
hooks.OnNodeStopping();
codexAccess.Stop(waitTillStopped);
}
public IDownloadedLog DownloadLog(string additionalName = "")
{
return codexAccess.DownloadLog(additionalName);
}
public Address GetDiscoveryEndpoint()
{
return codexAccess.GetDiscoveryEndpoint();
}
public Address GetApiEndpoint()
{
return codexAccess.GetApiEndpoint();
}
public Address GetListenEndpoint()
{
return codexAccess.GetListenEndpoint();
}
public Address GetMetricsScrapeTarget()
{
var address = codexAccess.GetMetricsEndpoint();
if (address == null) throw new Exception("Metrics ScrapeTarget accessed, but node was not started with EnableMetrics()");
return address;
}
public bool HasCrashed()
{
return codexAccess.HasCrashed();
}
public override string ToString()
{
return $"CodexNode:{GetName()}";
}
private void InitializePeerNodeId()
{
var debugInfo = Time.Retry(codexAccess.GetDebugInfo, "ensure online");
if (!debugInfo.Version.IsValid())
{
throw new Exception($"Invalid version information received from Codex node {GetName()}: {debugInfo.Version}");
}
peerId = debugInfo.Id;
nodeId = debugInfo.Table.LocalNode.NodeId;
Version = debugInfo.Version;
}
private void InitializeLogReplacements()
{
var nodeName = GetName();
log.AddStringReplace(peerId, nodeName);
log.AddStringReplace(CodexUtils.ToShortId(peerId), nodeName);
log.AddStringReplace(nodeId, nodeName);
log.AddStringReplace(CodexUtils.ToShortId(nodeId), nodeName);
}
private string[] GetPeerMultiAddresses(CodexNode peer, DebugInfo peerInfo)
{
var peerId = peer.GetDiscoveryEndpoint().Host
.Replace("http://", "")
.Replace("https://", "");
return peerInfo.Addrs.Select(a => a
.Replace("0.0.0.0", peerId))
.ToArray();
}
private void DownloadToFile(string contentId, TrackedFile file)
{
using var fileStream = File.OpenWrite(file.Filename);
var timeout = TimeSpan.FromMinutes(2.0); // todo: make this user-controllable.
try
{
// Type of stream generated by openAPI client does not support timeouts.
var start = DateTime.UtcNow;
var cts = new CancellationTokenSource();
var downloadTask = Task.Run(() =>
{
using var downloadStream = codexAccess.DownloadFile(contentId);
downloadStream.CopyTo(fileStream);
}, cts.Token);
while (DateTime.UtcNow - start < timeout)
{
if (downloadTask.IsFaulted) throw downloadTask.Exception;
if (downloadTask.IsCompletedSuccessfully) return;
Thread.Sleep(100);
}
cts.Cancel();
throw new TimeoutException($"Download of '{contentId}' timed out after {Time.FormatDuration(timeout)}");
}
catch (Exception ex)
{
Log($"Failed to download file '{contentId}': {ex}");
throw;
}
}
public void WaitUntilQuotaUsedIncreased(CodexSpace startSpace, ByteSize expectedIncreaseOfQuotaUsed)
{
WaitUntilQuotaUsedIncreased(startSpace, expectedIncreaseOfQuotaUsed, TimeSpan.FromMinutes(2));
}
public void WaitUntilQuotaUsedIncreased(
CodexSpace startSpace,
ByteSize expectedIncreaseOfQuotaUsed,
TimeSpan maxTimeout)
{
Log($"Waiting until quotaUsed " +
$"(start: {startSpace.QuotaUsedBytes}) " +
$"increases by {expectedIncreaseOfQuotaUsed} " +
$"to reach {startSpace.QuotaUsedBytes + expectedIncreaseOfQuotaUsed.SizeInBytes}");
var retry = new Retry($"Checking local space for quotaUsed increase of {expectedIncreaseOfQuotaUsed}",
maxTimeout: maxTimeout,
sleepAfterFail: TimeSpan.FromSeconds(3),
onFail: f => { });
retry.Run(() =>
{
var space = Space();
var increase = space.QuotaUsedBytes - startSpace.QuotaUsedBytes;
if (increase < expectedIncreaseOfQuotaUsed.SizeInBytes)
throw new Exception($"Expected quota-used not reached. " +
$"Expected increase: {expectedIncreaseOfQuotaUsed.SizeInBytes} " +
$"Actual increase: {increase} " +
$"Actual used: {space.QuotaUsedBytes}");
});
}
private void EnsureMarketplace()
{
if (codexAccess.GetEthAccount() == 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)
{
log.Log(msg);
}
}
}
@@ -0,0 +1,52 @@
using CodexClient.Hooks;
using FileUtils;
using Logging;
using WebUtils;
namespace CodexClient
{
public class CodexNodeFactory
{
private readonly ILog log;
private readonly IFileManager fileManager;
private readonly CodexHooksFactory hooksFactory;
private readonly IHttpFactory httpFactory;
private readonly IProcessControlFactory processControlFactory;
public CodexNodeFactory(ILog log, IFileManager fileManager, CodexHooksFactory hooksFactory, IHttpFactory httpFactory, IProcessControlFactory processControlFactory)
{
this.log = log;
this.fileManager = fileManager;
this.hooksFactory = hooksFactory;
this.httpFactory = httpFactory;
this.processControlFactory = processControlFactory;
}
public CodexNodeFactory(ILog log, HttpFactory httpFactory, string dataDir)
: this(log, new FileManager(log, dataDir), new CodexHooksFactory(), httpFactory, new DoNothingProcessControlFactory())
{
}
public CodexNodeFactory(ILog log, string dataDir)
: this(log, new HttpFactory(log), dataDir)
{
}
public ICodexNode CreateCodexNode(ICodexInstance instance)
{
var processControl = processControlFactory.CreateProcessControl(instance);
var access = new CodexAccess(log, httpFactory, processControl, instance);
var hooks = hooksFactory.CreateHooks(access.GetName());
var marketplaceAccess = CreateMarketplaceAccess(instance, access, hooks);
var node = new CodexNode(log, access, fileManager, marketplaceAccess, hooks);
node.Initialize();
return node;
}
private IMarketplaceAccess CreateMarketplaceAccess(ICodexInstance instance, CodexAccess access, ICodexNodeHooks hooks)
{
if (instance.EthAccount == null) return new MarketplaceUnavailable();
return new MarketplaceAccess(log, access, hooks);
}
}
}
@@ -1,7 +1,7 @@
using Newtonsoft.Json;
using Utils;
namespace CodexPlugin
namespace CodexClient
{
public class DebugInfo
{
@@ -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;
@@ -104,6 +109,16 @@ namespace CodexPlugin
{
return HashCode.Combine(Id);
}
public static bool operator ==(ContentId a, ContentId b)
{
return a.Id == b.Id;
}
public static bool operator !=(ContentId a, ContentId b)
{
return a.Id != b.Id;
}
}
public class CodexSpace

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