Compare commits
No commits in common. "01326b05ed54eca7402d1fa6ab3766b3187bb774" and "79e1a48d28593d351a566941638b875f5e962377" have entirely different histories.
01326b05ed
...
79e1a48d28
132 changed files with 3391 additions and 5582 deletions
|
|
@ -1,3 +1,3 @@
|
|||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
myelin-chat = "run --package xtask -- myelin-chat"
|
||||
mvp-chat = "run --package xtask -- mvp-chat"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
*
|
||||
!apps/
|
||||
!apps/myelin/node-image/
|
||||
!apps/myelin/node-image/**
|
||||
!apps/mvp-node/
|
||||
!apps/mvp-node/**
|
||||
!target/
|
||||
target/*
|
||||
!target/release/
|
||||
target/release/*
|
||||
!target/release/myelin-worker
|
||||
!target/release/mvp-node
|
||||
!target/release/mvp-worker-node
|
||||
!target/debug/
|
||||
target/debug/*
|
||||
!target/debug/myelin-worker
|
||||
!target/debug/mvp-node
|
||||
!target/debug/mvp-worker-node
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -2,7 +2,10 @@
|
|||
__pycache__
|
||||
fuzz/artifacts/**
|
||||
corpus
|
||||
.loop/
|
||||
.model-cache/
|
||||
.deployment-notes/
|
||||
.omp/
|
||||
|
||||
# environment variables for deployment configurations
|
||||
.config/
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -2517,7 +2517,7 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "myelin"
|
||||
name = "mvp-system"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
|
|
|
|||
25
Cargo.toml
25
Cargo.toml
|
|
@ -11,37 +11,16 @@ members = [
|
|||
"crates/datastream",
|
||||
"crates/data-plane",
|
||||
"crates/dashboard",
|
||||
"apps/myelin",
|
||||
"crates/mvp-system",
|
||||
"xtask",
|
||||
"tools/vastai",
|
||||
]
|
||||
default-members = [
|
||||
".",
|
||||
"crates/process",
|
||||
"crates/provisioning",
|
||||
"crates/transport",
|
||||
"crates/distribution",
|
||||
"crates/iroh-driver",
|
||||
"crates/datastream",
|
||||
"crates/data-plane",
|
||||
"crates/dashboard",
|
||||
"apps/myelin",
|
||||
"tools/vastai",
|
||||
]
|
||||
# The language bindings (python, wasm) and xtask are built on demand
|
||||
# (-p / --workspace / cargo-xtask), not during normal native iteration.
|
||||
exclude = ["crates/bindings/wasm-crypto", "examples"]
|
||||
[workspace.dependencies]
|
||||
# Centralized so every member resolves tokio with the SAME feature set.
|
||||
# Without this, members declared tokio with different features, so building
|
||||
# one member vs another (or `run` vs `test`) recompiled tokio each time.
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "process", "io-util", "sync", "time", "net", "signal"] }
|
||||
exclude = ["crates/bindings/wasm-crypto", "examples", "apps"]
|
||||
|
||||
[package]
|
||||
name = "swactor"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
autobenches = false
|
||||
|
||||
[profile.bench]
|
||||
|
|
|
|||
661
LICENSE
661
LICENSE
|
|
@ -1,661 +0,0 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
397
README.md
397
README.md
|
|
@ -1,392 +1,23 @@
|
|||
# swactor
|
||||
|
||||
A small, WASM-first actor runtime for Rust: one trait, location-transparent
|
||||
addresses, and a built-in cluster. Actors are ordinary structs; a message is
|
||||
any `Clone + Send + Sync` type; an address works the same whether the actor
|
||||
lives in this process, on a peer, or behind NAT on another node.
|
||||
Minimal actor runtime for Rust. One trait, one message type.
|
||||
Single-threaded (`tick()`) or multi-threaded (`run()`).
|
||||
|
||||
## What it is
|
||||
## Description
|
||||
|
||||
Every actor is one trait. There is no separate message trait to implement —
|
||||
anything `Clone + Send + Sync` is a message by blanket impl:
|
||||
Core runtime is `src/`. Actors implement `ActorInterface` (in `actor.rs`),
|
||||
interact through `Ctx` (in `runtime.rs`), and run on worker threads (`worker.rs`).
|
||||
|
||||
```rust
|
||||
// src/actor.rs
|
||||
pub trait Message: 'static + Sized + Clone + Send + Sync {}
|
||||
impl<T: 'static + Sized + Clone + Send + Sync> Message for T {}
|
||||
`crates/` builds upward: `std` adds OTP patterns (supervision, monitoring, groups),
|
||||
`distribution` adds clustering, everything else composes from there.
|
||||
|
||||
## Dev commands
|
||||
|
||||
`cargo xtask --help` for the basic test command.
|
||||
|
||||
## Testing
|
||||
|
||||
pub trait ActorInterface: 'static + Send {
|
||||
type Incoming: Message;
|
||||
type Response: Message;
|
||||
fn handle(&mut self, ctx: &Ctx, msg: Self::Incoming);
|
||||
// ...on_start, on_stop, handle_down (death monitoring) as needed
|
||||
}
|
||||
```
|
||||
|
||||
You drive the runtime single-threaded with `tick()` (this is what compiles to
|
||||
WASM) or multi-threaded with `run()`:
|
||||
|
||||
```rust
|
||||
use swactor::actor::{ActorInterface, Ctx};
|
||||
use swactor::runtime::{Runtime, RuntimeConfig};
|
||||
|
||||
struct Counter { count: u64 }
|
||||
|
||||
impl ActorInterface for Counter {
|
||||
type Incoming = (); // `()` is Clone + Send + Sync, so it's a message
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _: Self::Incoming) {
|
||||
self.count += 1;
|
||||
println!("hit #{self.count}");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), swactor::Error> {
|
||||
let rt = Runtime::new(RuntimeConfig::default()); // 1 worker → drive with tick()
|
||||
let addr = rt.spawn(Counter { count: 0 })?;
|
||||
rt.send_to(addr, ())?;
|
||||
for _ in 0..3 { rt.tick(); } // spawn → handle → done
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
`RuntimeConfig` is three knobs — `max_actors`, `channel_buffer_size`,
|
||||
and a per-tick `actor_message_budget` (inspired by BEAM's
|
||||
reduction count, so one chatty mailbox can't starve the rest).
|
||||
|
||||
## Features
|
||||
|
||||
Each feature is shown in the code that implements it.
|
||||
|
||||
### Route messages by actor_id across cluster boundaries
|
||||
|
||||
An actor's identity is a 32-byte address, not a pointer:
|
||||
|
||||
```rust
|
||||
// src/actor.rs
|
||||
pub struct ActorAddress(pub [u8; 32]);
|
||||
```
|
||||
|
||||
You send to it the same way whether it lives in this process, on a peer across
|
||||
the room, or behind NAT on another continent — one call, `ctx.send(addr, msg)`.
|
||||
The locality decision is exactly one address-map lookup; a miss hands the
|
||||
message to the cluster transport instead of a local worker:
|
||||
|
||||
```rust
|
||||
// src/delivery.rs — the cluster boundary
|
||||
fn route_nonlocal(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
|
||||
#[cfg(feature = "transport")]
|
||||
{
|
||||
if self.inbox_registry.contains(&addr) { // a pending reply inbox
|
||||
return self.inbox_registry.try_deliver(addr, msg);
|
||||
}
|
||||
if let Some(sink) = self.remote_sink { // ← off this node
|
||||
return sink.send(addr, msg);
|
||||
}
|
||||
}
|
||||
self.inbox_registry.try_deliver(addr, msg)
|
||||
}
|
||||
```
|
||||
|
||||
The transport seam is a single trait — any cluster driver satisfies it:
|
||||
|
||||
```rust
|
||||
// src/runtime.rs
|
||||
pub trait RemoteSink: Send + Sync {
|
||||
fn send(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
|
||||
}
|
||||
```
|
||||
|
||||
Where `addr` lives is resolved from a signed, gossiped directory: a host-signed
|
||||
`DirectoryEntry` binds `actor_addr → node_id`, backed by an LRU `LocationCache`.
|
||||
The node's own mailbox (`node_id → ActorAddress`) is rebound on restart without
|
||||
changing identity, which is why `NodeId` and `ActorAddress` are deliberately
|
||||
distinct types.
|
||||
|
||||
### WASM by default
|
||||
|
||||
The same runtime that runs on a thread pool also compiles to `wasm32` and steps
|
||||
one tick at a time from a host (browser, Node, an embedding app). Randomness,
|
||||
timers, and the address map all have a no-op/wasm path behind the `wasm`
|
||||
feature. The [ping-pong demo](examples/ping-pong) is one runtime compiled to a
|
||||
`.wasm` cdylib and driven by Node:
|
||||
|
||||
```sh
|
||||
cd examples/ping-pong && ./run.sh
|
||||
```
|
||||
|
||||
### Built-in engine and driver, tasks included
|
||||
|
||||
A node ships batteries-included: the actor engine (worker threads), a network
|
||||
driver, and the pump/fanout tasks that wire them — you don't assemble the glue.
|
||||
The driver tracks its own established send/recv pumps per QUIC stream and emits
|
||||
lifecycle events (edge ready, stream fault, pump stopped):
|
||||
|
||||
```rust
|
||||
// crates/iroh-driver/src/driver_pumps.rs
|
||||
pub enum DriverEventOut {
|
||||
DriverEdgeReady { edge_id: EdgeId },
|
||||
StreamFault { edge_id: EdgeId },
|
||||
PumpStopped { edge_id: EdgeId, ring_id: RingId },
|
||||
}
|
||||
```
|
||||
|
||||
### `iroh` integration — QUIC, TLS, and NAT traversal
|
||||
|
||||
The bundled driver is [iroh](https://iroh.computer): QUIC-based peer transport
|
||||
with built-in TLS, NAT hole-punching, and relay-server fallback. You hand it a
|
||||
Tokio engine and it runs accepts, dials, stream I/O, retries, and shutdown on
|
||||
it; peers are admitted through an optional allow-list:
|
||||
|
||||
```rust
|
||||
// crates/iroh-driver/src/iroh_driver.rs
|
||||
pub struct IrohDriverConfig {
|
||||
pub secret_key: Option<SecretKey>, // None → fresh random identity
|
||||
pub relay_mode: RelayMode, // default: n0 production relays
|
||||
pub node: DistributedNodeConfig,
|
||||
pub peer_auth: Option<Arc<Mutex<PeerAllowList>>>,
|
||||
pub additional_alpns: Vec<Vec<u8>>, // opaque to the driver
|
||||
}
|
||||
```
|
||||
|
||||
### Built-in SWIM cluster
|
||||
|
||||
Membership is a real SWIM implementation — direct + indirect (relay) probes,
|
||||
suspicion timers, and Lifeguard local-health scaling — not heartbeats. The probe
|
||||
state machine emits typed actions; the host adapter turns them into sends:
|
||||
|
||||
```rust
|
||||
// crates/distribution/src/swim/probe.rs
|
||||
pub enum SwimAction {
|
||||
SendPing { to: NodeId, sequence: u64 },
|
||||
SendPingReq { relay: NodeId, target: NodeId, sequence: u64 }, // indirect probe
|
||||
Suspect(NodeId),
|
||||
DeclareDead(NodeId),
|
||||
Diag(SwimDiagEvent), // RTT samples — observation only, no protocol effect
|
||||
}
|
||||
```
|
||||
|
||||
Name bindings, node metadata (relay URL, human name), and the actor→host
|
||||
directory each run as independent gossip actors, so the cluster goes quiet once
|
||||
it converges.
|
||||
|
||||
### OTP-style standard library
|
||||
|
||||
Behind the `std` feature, one extension adds the patterns you reach for:
|
||||
symbolic naming, death monitoring (watching), and process groups. Install it on
|
||||
the runtime and actors get `CtxWatching` / `CtxNaming`:
|
||||
|
||||
```rust
|
||||
// src/std/extension.rs
|
||||
pub struct StdExtension {
|
||||
name_registry: NameRegistry, // logical names → addresses
|
||||
watch_registry: WatchRegistry, // exit notifications
|
||||
group_registry: GroupRegistry, // named groups / pub-sub
|
||||
}
|
||||
// runtime.with_extension(Arc::new(StdExtension::new()));
|
||||
```
|
||||
|
||||
### Process manager for external processes
|
||||
|
||||
Treat an OS process like an actor. Describe it, spawn it onto the runtime, and
|
||||
send it commands; its lifecycle (started / exited / signaled) arrives as
|
||||
messages:
|
||||
|
||||
```rust
|
||||
// crates/process/src/types.rs
|
||||
pub struct ProcessSpec {
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub env: HashMap<String, String>,
|
||||
pub working_dir: Option<std::path::PathBuf>,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
There's a YAML-driven pipeline layer on top (`crates/process/src/pipeline.rs`)
|
||||
for multi-step jobs with progress and failure reporting.
|
||||
|
||||
### Zero-copy movement of large byte objects
|
||||
|
||||
The data-plane ships large, *typed* byte objects between actors — and across
|
||||
nodes — without copying. The motivating workload is sharded inference: one GPU's
|
||||
output activation is the next GPU's input, so it rides a bounded ring leased from
|
||||
a shared arena. A ring is either local (IPC between two actors on one node) or
|
||||
the two ends of a QUIC edge:
|
||||
|
||||
```text
|
||||
producer → egress ring ──QUIC──▶ ingress ring → consumer
|
||||
(arena lease; zero-copy at both ends)
|
||||
```
|
||||
|
||||
What travels on a wire edge is typed, so the receiver knows what arrived:
|
||||
|
||||
```rust
|
||||
// crates/data-plane/src/actor.rs
|
||||
pub enum EdgeKind {
|
||||
TokenIn,
|
||||
Activation, // a hidden-state tensor forwarded to the next shard
|
||||
TokenOut,
|
||||
}
|
||||
|
||||
// crates/data-plane/src/edge_lifecycle.rs
|
||||
pub struct ObjectSpec {
|
||||
pub kind: ObjectKind, // Activation
|
||||
pub dtype: DType, // F16
|
||||
pub max_extent_bytes: u64,
|
||||
}
|
||||
```
|
||||
|
||||
The edge actor owns the lifecycle: lease a byte range, install an ingress or
|
||||
egress ring, and release it only with a `QuiescenceProof`, so a range is never
|
||||
recycled under a live reader. The worker consumes whole objects off its ingress
|
||||
ring as plain bytes:
|
||||
|
||||
```rust
|
||||
// apps/myelin/src/node/worker_node_runtime.rs
|
||||
match ingress::read_object_record(buffer, object_spec, false)? {
|
||||
ingress::ObjectRecordRead::Incomplete => Ok(None), // wait for more bytes
|
||||
ingress::ObjectRecordRead::Complete(record) => { // a full activation arrived
|
||||
let bytes: Vec<u8> = buffer.drain(..record.total_len).collect();
|
||||
Ok(Some(IngressRecordBytes { bytes, object_id: record.object_id.0, .. }))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom metrics through the datastream
|
||||
|
||||
Telemetry is a deliberately dumb pipe: producers tag bytes with a channel id,
|
||||
nothing in the middle interprets the payload, and views are read-time
|
||||
projections. To add a metric stream, define a type and implement one constant —
|
||||
no schema registry to negotiate with:
|
||||
|
||||
```rust
|
||||
// crates/datastream/src/record.rs
|
||||
pub trait Record {
|
||||
const CHANNEL: &'static str;
|
||||
}
|
||||
|
||||
// crates/data-plane/src/arena.rs — the arena publishes its own health this way
|
||||
impl Record for ArenaSample {
|
||||
const CHANNEL: &'static str = ARENA_SAMPLE_CHANNEL; // "mvp.arena"
|
||||
}
|
||||
```
|
||||
|
||||
### Built-in dashboard
|
||||
|
||||
A read-only HTML/SSE dashboard renders the frames the datastream already
|
||||
carries — workers, the actor roster, per-actor dossiers, fleet hardware. It sends
|
||||
no control signals back, so the whole thing is one Axum router:
|
||||
|
||||
```rust
|
||||
// crates/dashboard/src/server.rs
|
||||
fn router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(root_page))
|
||||
.route("/events", get(frame_stream)) // live frames over SSE
|
||||
.route("/api/frames", get(recent_frames))
|
||||
.route("/api/views", get(views_json))
|
||||
.route("/api/view/{*path}", get(view_snapshot))
|
||||
.route("/view/{*path}", get(view_page))
|
||||
.with_state(state)
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The workspace builds upward from a tiny core. Each crate is one responsibility:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
app["myelin<br/>node · orchestrator"]
|
||||
driver["iroh-driver<br/>QUIC network driver"]
|
||||
dist["distribution<br/>SWIM · registry · directory"]
|
||||
dash["dashboard<br/>read-only view over datastream"]
|
||||
proc["process<br/>managed external processes"]
|
||||
dp["data-plane<br/>zero-copy byte movement · IPC · links"]
|
||||
ds["datastream<br/>metrics / telemetry pipe"]
|
||||
trans["transport<br/>codec · identity · crypto"]
|
||||
core["swactor<br/>core runtime + std (OTP)<br/>ActorInterface · Ctx"]
|
||||
|
||||
app --> driver
|
||||
app --> dist
|
||||
app --> dp
|
||||
app --> dash
|
||||
app --> proc
|
||||
driver --> dist
|
||||
driver --> ds
|
||||
dist --> trans
|
||||
dist --> ds
|
||||
dash --> ds
|
||||
proc --> ds
|
||||
ds --> trans
|
||||
trans --> core
|
||||
dp --> core
|
||||
ds --> core
|
||||
proc --> core
|
||||
```
|
||||
|
||||
| Crate | Role |
|
||||
| --- | --- |
|
||||
| `swactor` (`src/`) | Core runtime: the actor trait, `Ctx`, scheduler, address map, channels |
|
||||
| `crates/transport` | Codec registry, node identity, message signing — transport-agnostic |
|
||||
| `crates/distribution` | Clustering: SWIM membership, naming, metadata + actor directory gossip |
|
||||
| `crates/iroh-driver` | iroh/QUIC network driver with its pump/fanout tasks |
|
||||
| `crates/datastream` | Metrics / telemetry pipe — nothing in the middle interprets payloads |
|
||||
| `crates/data-plane` | Zero-copy movement of large typed byte objects over IPC or a network link |
|
||||
| `crates/process` | Managed external processes and YAML pipelines |
|
||||
| `crates/dashboard` | Read-only HTML/SSE dashboard over datastream frames |
|
||||
| `crates/provisioning` | Cloud node provisioning |
|
||||
| `crates/bindings/{python,wasm-runtime,wasm-crypto}` | Language / target bindings |
|
||||
| `apps/myelin` | The node binary that wires the above into a runnable cluster node |
|
||||
| `tools/vastai` | Vast.ai GPU-marketplace tooling |
|
||||
|
||||
## Used for
|
||||
|
||||
- **A drop-in replacement for GPU jobs on cloud infrastructure.** `apps/myelin`
|
||||
ships an orchestrator plus a containerized worker node
|
||||
(`apps/myelin/node-image`) that provisions, stages models, and runs them.
|
||||
- **Orchestration for sharded inference.** The node image includes a
|
||||
[tinygrad](https://github.com/tinygrad/tinygrad) worker
|
||||
(`apps/myelin/node-image/tinygrad_worker.py`); a vLLM backend is planned.
|
||||
|
||||
## Examples
|
||||
|
||||
**Working:** [`examples/ping-pong`](examples/ping-pong) — two actors volley on a
|
||||
single-threaded runtime compiled to WebAssembly, driven tick-by-tick from Node.
|
||||
Shows spawning, message passing, and death monitoring in one file.
|
||||
|
||||
**Planned** (tracked in `.deployment-notes/ROADMAP.md`):
|
||||
|
||||
- Two actors talking in-process, over IPC, and across a WAN — in Rust, Python,
|
||||
and WASM — from the same code.
|
||||
- Typed chunk streaming across IPC or WAN.
|
||||
- A non-trivially sharded training job (not easily expressed in Ray).
|
||||
- A full pipeline-parallel example of a model sharded across N ≥ 3 cheap GPUs.
|
||||
|
||||
## Developing
|
||||
|
||||
Requires the pinned nightly toolchain (`rust-toolchain.toml`):
|
||||
|
||||
```sh
|
||||
cargo check --workspace
|
||||
cargo xtask test # see `cargo xtask --help`
|
||||
cargo xtask test
|
||||
```
|
||||
|
||||
The language bindings (`crates/bindings/*`) build on demand with `-p` /
|
||||
`--workspace`, not during normal native iteration.
|
||||
|
||||
## Status
|
||||
|
||||
swactor is early and under active development. The runtime, clustering, and the
|
||||
`myelin` node are exercised by the workspace test suite, but APIs are still
|
||||
moving.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [GNU Affero General Public License v3.0 only]
|
||||
(./LICENSE) (`AGPL-3.0-only`). The AGPL's network clause (§13) requires anyone
|
||||
who runs a modified swactor as a network service to offer its source to users.
|
||||
The copyright holder is not bound by their own license and may relicense future
|
||||
releases freely; external contributors should expect a CLA requirement before
|
||||
changes are merged.
|
||||
|
|
|
|||
15
apps/mvp-node/Dockerfile
Normal file
15
apps/mvp-node/Dockerfile
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# MVP node CODE image — thin layer over the CUDA/tinygrad base.
|
||||
# Build from workspace root after compiling the Rust binary:
|
||||
# cargo build --release -p mvp-system --bin mvp-worker-node
|
||||
# docker build -f apps/mvp-node/Dockerfile.base -t swactor-mvp-node-base:cuda12.6 .
|
||||
# docker build -f apps/mvp-node/Dockerfile --build-arg BASE_IMAGE=swactor-mvp-node-base:cuda12.6 -t swactor-mvp-node:latest .
|
||||
|
||||
ARG BASE_IMAGE=swactor-mvp-node-base:cuda12.6
|
||||
ARG MVP_NODE_BIN=target/release/mvp-worker-node
|
||||
FROM ${BASE_IMAGE}
|
||||
ARG MVP_NODE_BIN=target/release/mvp-worker-node
|
||||
|
||||
COPY ${MVP_NODE_BIN} /usr/local/bin/mvp-node
|
||||
COPY apps/mvp-node/tinygrad_worker.py /usr/local/share/mvp/tinygrad_worker.py
|
||||
|
||||
RUN chmod +x /usr/local/bin/mvp-node /usr/local/share/mvp/tinygrad_worker.py
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Myelin node BASE image — CUDA/Python/tinygrad/sshd/PID-1 foundation.
|
||||
# MVP node BASE image — CUDA/Python/tinygrad/sshd/PID-1 foundation.
|
||||
# Build from workspace root:
|
||||
# docker build -f apps/myelin/node-image/Dockerfile.base -t myelin-node-base:cuda12.6 .
|
||||
# docker build -f apps/mvp-node/Dockerfile.base -t swactor-mvp-node-base:cuda12.6 .
|
||||
|
||||
FROM nvidia/cuda:12.6.3-runtime-ubuntu24.04 AS builder
|
||||
|
||||
|
|
@ -13,17 +13,17 @@ RUN apt-get update && \
|
|||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN python3 -m pip install --no-cache-dir --break-system-packages \
|
||||
--target=/opt/myelin-pydeps \
|
||||
--target=/opt/mvp-pydeps \
|
||||
tinygrad==0.12.0 numpy && \
|
||||
find /opt/myelin-pydeps -depth -type d \
|
||||
find /opt/mvp-pydeps -depth -type d \
|
||||
\( -name '__pycache__' -o -name 'tests' -o -name 'test' \) \
|
||||
-exec rm -rf {} + && \
|
||||
find /opt/myelin-pydeps -name '*.pyc' -delete && \
|
||||
find /opt/myelin-pydeps -type d -name '*.dist-info' -exec rm -rf {} +
|
||||
find /opt/mvp-pydeps -name '*.pyc' -delete && \
|
||||
find /opt/mvp-pydeps -type d -name '*.dist-info' -exec rm -rf {} +
|
||||
|
||||
RUN mkdir -p /opt/myelin-nvrtc-include && \
|
||||
cp -rL /usr/local/cuda/include/. /opt/myelin-nvrtc-include/ && \
|
||||
test -f /opt/myelin-nvrtc-include/vector_types.h
|
||||
RUN mkdir -p /opt/mvp-nvrtc-include && \
|
||||
cp -rL /usr/local/cuda/include/. /opt/mvp-nvrtc-include/ && \
|
||||
test -f /opt/mvp-nvrtc-include/vector_types.h
|
||||
|
||||
FROM nvidia/cuda:12.6.3-base-ubuntu24.04 AS runtime
|
||||
|
||||
|
|
@ -48,21 +48,21 @@ RUN apt-get update && \
|
|||
/usr/share/info/* && \
|
||||
find /usr -depth -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \
|
||||
find /usr -type f -name '*.pyc' -delete 2>/dev/null || true && \
|
||||
mkdir -p /usr/local/share/myelin /var/cache/myelin-models /var/log
|
||||
mkdir -p /usr/local/share/mvp /var/cache/mvp-models /var/log
|
||||
|
||||
COPY --from=builder /opt/myelin-nvrtc-include/ /usr/local/cuda/include/
|
||||
COPY --from=builder /opt/myelin-pydeps /opt/myelin-pydeps
|
||||
ENV PYTHONPATH=/opt/myelin-pydeps
|
||||
COPY --from=builder /opt/mvp-nvrtc-include/ /usr/local/cuda/include/
|
||||
COPY --from=builder /opt/mvp-pydeps /opt/mvp-pydeps
|
||||
ENV PYTHONPATH=/opt/mvp-pydeps
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV CUDA=1
|
||||
ENV DEV=CUDA
|
||||
ENV MYELIN_MODEL_CACHE_DIR=/var/cache/myelin-models
|
||||
ENV MYELIN_TINYGRAD_WORKER=/usr/local/share/myelin/tinygrad_worker.py
|
||||
ENV MVP_MODEL_CACHE_DIR=/var/cache/mvp-models
|
||||
ENV MVP_TINYGRAD_WORKER=/usr/local/share/mvp/tinygrad_worker.py
|
||||
|
||||
COPY apps/myelin/node-image/myelin_entrypoint.sh /usr/local/bin/myelin_entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/myelin_entrypoint.sh
|
||||
COPY apps/mvp-node/mvp_entrypoint.sh /usr/local/bin/mvp_entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/mvp_entrypoint.sh
|
||||
|
||||
ARG DEPLOY_PUBKEY=""
|
||||
RUN if [ -n "$DEPLOY_PUBKEY" ]; then printf '%s\n' "$DEPLOY_PUBKEY" > /etc/myelin_deploy_key.pub; fi
|
||||
RUN if [ -n "$DEPLOY_PUBKEY" ]; then printf '%s\n' "$DEPLOY_PUBKEY" > /etc/mvp_deploy_key.pub; fi
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/myelin_entrypoint.sh"]
|
||||
ENTRYPOINT ["/usr/local/bin/mvp_entrypoint.sh"]
|
||||
54
apps/mvp-node/local_docker_e2e.sh
Executable file
54
apps/mvp-node/local_docker_e2e.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_IMAGE=${BASE_IMAGE:-swactor-mvp-node-base:cuda12.6}
|
||||
IMAGE=${IMAGE:-swactor-mvp-node:latest}
|
||||
CONTAINER=${CONTAINER:-swactor-mvp-node-e2e-$$}
|
||||
GPUS=${MVP_CUDA_GPUS:-all}
|
||||
PROMPT=${MVP_NODE_SELF_TEST_PROMPT:-ping}
|
||||
TIMEOUT_SECS=${MVP_NODE_E2E_TIMEOUT_SECS:-1800}
|
||||
FRAME_LOG=${MVP_DATASTREAM_FRAME_LOG:-/var/log/mvp-datastream.ndjson}
|
||||
|
||||
cleanup() {
|
||||
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cargo build --release -p mvp-system --bin mvp-worker-node
|
||||
docker build -f apps/mvp-node/Dockerfile.base -t "$BASE_IMAGE" .
|
||||
docker build -f apps/mvp-node/Dockerfile --build-arg BASE_IMAGE="$BASE_IMAGE" -t "$IMAGE" .
|
||||
|
||||
docker run -d \
|
||||
--name "$CONTAINER" \
|
||||
--gpus "$GPUS" \
|
||||
-e MVP_NODE_SELF_TEST_PROMPT="$PROMPT" \
|
||||
-e MVP_NODE_MAX_RUNTIME_SECS=1 \
|
||||
-e MVP_SELF_TEST_MAX_TOKENS="${MVP_SELF_TEST_MAX_TOKENS:-1}" \
|
||||
-e MVP_MODEL_CACHE_DIR=/var/cache/mvp-models \
|
||||
-e MVP_DATASTREAM_FRAME_LOG="$FRAME_LOG" \
|
||||
${HF_TOKEN:+-e HF_TOKEN="$HF_TOKEN"} \
|
||||
"$IMAGE" >/dev/null
|
||||
|
||||
deadline=$((SECONDS + TIMEOUT_SECS))
|
||||
while (( SECONDS < deadline )); do
|
||||
logs=$(docker logs "$CONTAINER" 2>&1 || true)
|
||||
if grep -q '"type":"ready"' <<<"$logs" && grep -q '"type":"self_test_completed"' <<<"$logs"; then
|
||||
frames=$(docker exec "$CONTAINER" cat "$FRAME_LOG" 2>/dev/null || true)
|
||||
if grep -q '"channel":"mvp.node.ready"' <<<"$frames" &&
|
||||
grep -q '"channel":"mvp.worker.weights"' <<<"$frames" &&
|
||||
grep -q '"channel":"mvp.worker.prompt"' <<<"$frames"; then
|
||||
printf '%s\n' "$logs"
|
||||
printf '%s\n' "$frames"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
if grep -q 'WorkerFatal\|mvp-node: .*failed\|ModelLoadFailed\|GgufDownloadFailed' <<<"$logs"; then
|
||||
printf '%s\n' "$logs" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
docker logs "$CONTAINER" 2>&1 || true
|
||||
echo "mvp-node Docker E2E timed out after ${TIMEOUT_SECS}s" >&2
|
||||
exit 1
|
||||
34
apps/mvp-node/mvp_entrypoint.sh
Executable file
34
apps/mvp-node/mvp_entrypoint.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
if ! mkdir /run/mvp_entrypoint.lock 2>/dev/null; then
|
||||
echo "mvp-entrypoint: already running (lock held); parking" >&2
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||
: > /root/.ssh/authorized_keys
|
||||
if [ -n "${PUBLIC_KEY:-}" ]; then printf '%s\n' "$PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
|
||||
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then printf '%s\n' "$SSH_PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
|
||||
if [ -f /etc/mvp_deploy_key.pub ]; then cat /etc/mvp_deploy_key.pub >> /root/.ssh/authorized_keys; fi
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
|
||||
if [ ! -s /root/.ssh/authorized_keys ]; then
|
||||
echo "mvp-entrypoint: WARNING no SSH public key found (PUBLIC_KEY/SSH_PUBLIC_KEY unset, no baked key); SSH will reject logins" >&2
|
||||
fi
|
||||
|
||||
mkdir -p /run/sshd /var/log /var/cache/mvp-models
|
||||
ssh-keygen -A 2>/dev/null || true
|
||||
/usr/sbin/sshd -e
|
||||
echo "mvp-entrypoint: sshd up on :22" >&2
|
||||
|
||||
NODE_LOG=/var/log/mvp-node.log
|
||||
echo "mvp-entrypoint: launching mvp-node (log -> $NODE_LOG)" >&2
|
||||
set -o pipefail
|
||||
/usr/local/bin/mvp-node "$@" 2>&1 | tee "$NODE_LOG"
|
||||
code=${PIPESTATUS[0]}
|
||||
|
||||
echo "mvp-entrypoint: mvp-node exited with code $code; NOT restarting (node held for postmortem)" >&2
|
||||
echo "mvp-entrypoint: --- last 80 lines of $NODE_LOG ---" >&2
|
||||
tail -n 80 "$NODE_LOG" >&2 || true
|
||||
exec sleep infinity
|
||||
|
|
@ -107,8 +107,8 @@ def start_cpu_line_sampler(
|
|||
request_id: int | None,
|
||||
model_id: str | None,
|
||||
) -> CpuLineSampler | None:
|
||||
raw = os.environ.get("MYELIN_CPU_LINE_PROFILE")
|
||||
if not env_flag("MYELIN_CPU_LINE_PROFILE", False):
|
||||
raw = os.environ.get("MVP_CPU_LINE_PROFILE")
|
||||
if not env_flag("MVP_CPU_LINE_PROFILE", False):
|
||||
control(
|
||||
type="CpuLineProfileSkipped",
|
||||
phase=phase,
|
||||
|
|
@ -117,7 +117,7 @@ def start_cpu_line_sampler(
|
|||
env_value=raw,
|
||||
)
|
||||
return None
|
||||
interval_ms = float(os.environ.get("MYELIN_CPU_LINE_PROFILE_INTERVAL_MS", "2"))
|
||||
interval_ms = float(os.environ.get("MVP_CPU_LINE_PROFILE_INTERVAL_MS", "2"))
|
||||
interval_secs = max(0.0005, interval_ms / 1000.0)
|
||||
control(
|
||||
type="CpuLineProfileStarted",
|
||||
|
|
@ -158,7 +158,7 @@ def benchmark_stamp() -> dict[str, Any]:
|
|||
"component": "tinygrad-worker",
|
||||
"producer_component": "tinygrad-worker",
|
||||
"producer_instance_id": os.environ.get(
|
||||
"MYELIN_BENCHMARK_PRODUCER_INSTANCE",
|
||||
"MVP_BENCHMARK_PRODUCER_INSTANCE",
|
||||
f"tinygrad-worker:{pid}",
|
||||
),
|
||||
"producer_process_id": pid,
|
||||
|
|
@ -190,12 +190,12 @@ def datastream_endpoint_snapshot() -> dict[str, Any]:
|
|||
return {
|
||||
"role": "python-worker-stdio-json-bridge",
|
||||
"transport": "stdout-json-lines",
|
||||
"endpoint_identity": os.environ.get("MYELIN_DATASTREAM_ENDPOINT_ID", "worker-stdio-bridge"),
|
||||
"endpoint_identity": os.environ.get("MVP_DATASTREAM_ENDPOINT_ID", "worker-stdio-bridge"),
|
||||
"configured_source": "worker-node-env",
|
||||
"resolved_source": "TinygradWorker::spawn environment",
|
||||
"authentication_present": False,
|
||||
"tls_present": False,
|
||||
"relay_mode": os.environ.get("MYELIN_IROH_RELAY_MODE"),
|
||||
"relay_mode": os.environ.get("MVP_IROH_RELAY_MODE"),
|
||||
"endpoint_addr_mask": os.environ.get("MVP_IROH_ENDPOINT_ADDR_MASK"),
|
||||
"connectivity_result": "configured",
|
||||
}
|
||||
|
|
@ -225,17 +225,17 @@ def apply_canonical_envelope(event: dict[str, Any]) -> None:
|
|||
|
||||
def control(**event: Any) -> None:
|
||||
apply_canonical_envelope(event)
|
||||
if (run_id := env_int("MYELIN_RUN_ID")) is not None:
|
||||
if (run_id := env_int("MVP_RUN_ID")) is not None:
|
||||
event.setdefault("run_id", run_id)
|
||||
if (node_id := env_int("MYELIN_LOGICAL_NODE_ID")) is not None:
|
||||
if (node_id := env_int("MVP_LOGICAL_NODE_ID")) is not None:
|
||||
event.setdefault("node_id", node_id)
|
||||
if (stage_index := env_int("MYELIN_STAGE_INDEX")) is not None:
|
||||
if (stage_index := env_int("MVP_STAGE_INDEX")) is not None:
|
||||
event.setdefault("stage_index", stage_index)
|
||||
print(json.dumps(event, separators=(",", ":")), flush=True)
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
print(f"myelin_tinygrad_worker: {message}", file=sys.stderr, flush=True)
|
||||
print(f"mvp_tinygrad_worker: {message}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def fatal(reason: str, **fields: Any) -> None:
|
||||
|
|
@ -273,9 +273,9 @@ def initialize(cmd: dict[str, Any]) -> None:
|
|||
fatal("UnsupportedHelperAbi", helper_abi_version=cmd.get("helper_abi_version"))
|
||||
requested_device = str(cmd.get("backend", {}).get("device") or os.environ.get("DEV") or "CUDA")
|
||||
device = select_tinygrad_device(requested_device)
|
||||
arena_fd = os.environ.get("MYELIN_ARENA_FD")
|
||||
arena_fd = os.environ.get("MVP_ARENA_FD")
|
||||
if arena_fd is not None:
|
||||
arena_bytes = int(os.environ.get("MYELIN_ARENA_BYTES", "0") or "0")
|
||||
arena_bytes = int(os.environ.get("MVP_ARENA_BYTES", "0") or "0")
|
||||
if arena_bytes > 0:
|
||||
arena = mmap.mmap(int(arena_fd), arena_bytes)
|
||||
started = time.monotonic()
|
||||
|
|
@ -311,8 +311,8 @@ def configure_role(cmd: dict[str, Any]) -> None:
|
|||
|
||||
|
||||
def cache_root() -> Path:
|
||||
raw = os.environ.get("MYELIN_MODEL_CACHE_DIR", "").strip()
|
||||
root = Path(raw).expanduser() if raw else Path.home() / ".cache" / "myelin-node"
|
||||
raw = os.environ.get("MVP_MODEL_CACHE_DIR", "").strip()
|
||||
root = Path(raw).expanduser() if raw else Path.home() / ".cache" / "mvp-node"
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
return root
|
||||
|
||||
|
|
@ -351,7 +351,7 @@ def cache_path_for(url: str) -> Path:
|
|||
|
||||
|
||||
def request_headers() -> dict[str, str]:
|
||||
headers = {"User-Agent": "myelin-node/0.1"}
|
||||
headers = {"User-Agent": "swactor-mvp-node/0.1"}
|
||||
token = os.environ.get("HF_TOKEN", "").strip()
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
|
@ -392,7 +392,7 @@ def fetch_whole(source: dict[str, Any]) -> Path:
|
|||
out.write(chunk)
|
||||
done += len(chunk)
|
||||
now = time.monotonic()
|
||||
if now - last_event >= float(os.environ.get("MYELIN_DOWNLOAD_PROGRESS_SECS", "5")):
|
||||
if now - last_event >= float(os.environ.get("MVP_DOWNLOAD_PROGRESS_SECS", "5")):
|
||||
control(
|
||||
type="GgufDownloadProgress",
|
||||
bytes_done=done,
|
||||
|
|
@ -672,7 +672,7 @@ def load_weights(cmd: dict[str, Any]) -> None:
|
|||
llm_backend = "tinygrad.apps.llm"
|
||||
|
||||
control(type="TinygradLlmImportReady", model_id=model_id, backend=llm_backend)
|
||||
max_context_raw = os.environ.get("MYELIN_MAX_CONTEXT", "512")
|
||||
max_context_raw = os.environ.get("MVP_MAX_CONTEXT", "512")
|
||||
max_context = int(max_context_raw) if max_context_raw else 512
|
||||
control(
|
||||
type="PipelineStageFromGgufStarted",
|
||||
|
|
@ -744,7 +744,7 @@ def load_weights(cmd: dict[str, Any]) -> None:
|
|||
|
||||
|
||||
def prompt_template_name() -> str:
|
||||
explicit = os.environ.get("MYELIN_PROMPT_TEMPLATE")
|
||||
explicit = os.environ.get("MVP_PROMPT_TEMPLATE")
|
||||
if explicit is not None:
|
||||
return explicit.strip().lower()
|
||||
tokenizer_pre = str(loaded.get("tokenizer_pre", "")).lower()
|
||||
|
|
@ -911,7 +911,7 @@ def infer_prompt(cmd: dict[str, Any]) -> None:
|
|||
prompt_tokens=len(prompt_tokens),
|
||||
elapsed_ms=int((time.monotonic() - encode_started) * 1000),
|
||||
)
|
||||
progress_every = int(os.environ.get("MYELIN_TOKEN_PROGRESS_EVERY", "16") or "16")
|
||||
progress_every = int(os.environ.get("MVP_TOKEN_PROGRESS_EVERY", "16") or "16")
|
||||
decode_started = time.monotonic()
|
||||
control(
|
||||
type="DecodeStarted",
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
[package]
|
||||
name = "myelin"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
publish = false
|
||||
autobins = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
dashboard = []
|
||||
|
||||
[dependencies]
|
||||
datastream = { path = "../../crates/datastream" }
|
||||
data-plane = { path = "../../crates/data-plane" }
|
||||
provisioning = { path = "../../crates/provisioning" }
|
||||
dashboard = { path = "../../crates/dashboard" }
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
swactor = { path = "../..", features = ["serde", "transport"] }
|
||||
swactor-transport = { path = "../../crates/transport" }
|
||||
swactor-process = { path = "../../crates/process" }
|
||||
distribution = { path = "../../crates/distribution" }
|
||||
iroh-driver = { path = "../../crates/iroh-driver" }
|
||||
iroh = "0.98"
|
||||
tokio.workspace = true
|
||||
swactor-vastai = { path = "../../tools/vastai" }
|
||||
parking_lot = "0.12"
|
||||
blake3 = "1"
|
||||
toml = "0.8"
|
||||
ureq = "2"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
signal-hook = "0.3"
|
||||
|
||||
[[bin]]
|
||||
name = "myelin-worker"
|
||||
path = "src/bin/worker_node.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "myelin-orchestrator"
|
||||
path = "src/bin/orchestrator.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "myelin-chat"
|
||||
path = "src/bin/chat.rs"
|
||||
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# Myelin node CODE image — thin layer over the CUDA/tinygrad base.
|
||||
# Build from workspace root after compiling the Rust binary:
|
||||
# cargo build --release -p myelin --bin myelin-worker
|
||||
# docker build -f apps/myelin/node-image/Dockerfile.base -t myelin-node-base:cuda12.6 .
|
||||
# docker build -f apps/myelin/node-image/Dockerfile --build-arg BASE_IMAGE=myelin-node-base:cuda12.6 -t myelin-node:latest .
|
||||
|
||||
ARG BASE_IMAGE=myelin-node-base:cuda12.6
|
||||
ARG MYELIN_NODE_BIN=target/release/myelin-worker
|
||||
FROM ${BASE_IMAGE}
|
||||
ARG MYELIN_NODE_BIN=target/release/myelin-worker
|
||||
|
||||
COPY ${MYELIN_NODE_BIN} /usr/local/bin/myelin-node
|
||||
COPY apps/myelin/node-image/tinygrad_worker.py /usr/local/share/myelin/tinygrad_worker.py
|
||||
|
||||
RUN chmod +x /usr/local/bin/myelin-node /usr/local/share/myelin/tinygrad_worker.py
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_IMAGE=${BASE_IMAGE:-myelin-node-base:cuda12.6}
|
||||
IMAGE=${IMAGE:-myelin-node:latest}
|
||||
CONTAINER=${CONTAINER:-myelin-node-e2e-$$}
|
||||
GPUS=${MYELIN_CUDA_GPUS:-all}
|
||||
PROMPT=${MYELIN_NODE_SELF_TEST_PROMPT:-ping}
|
||||
TIMEOUT_SECS=${MYELIN_NODE_E2E_TIMEOUT_SECS:-1800}
|
||||
FRAME_LOG=${MYELIN_DATASTREAM_FRAME_LOG:-/var/log/myelin-datastream.ndjson}
|
||||
|
||||
cleanup() {
|
||||
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cargo build --release -p myelin --bin myelin-worker
|
||||
docker build -f apps/myelin/node-image/Dockerfile.base -t "$BASE_IMAGE" .
|
||||
docker build -f apps/myelin/node-image/Dockerfile --build-arg BASE_IMAGE="$BASE_IMAGE" -t "$IMAGE" .
|
||||
|
||||
docker run -d \
|
||||
--name "$CONTAINER" \
|
||||
--gpus "$GPUS" \
|
||||
-e MYELIN_NODE_SELF_TEST_PROMPT="$PROMPT" \
|
||||
-e MYELIN_NODE_MAX_RUNTIME_SECS=1 \
|
||||
-e MYELIN_SELF_TEST_MAX_TOKENS="${MYELIN_SELF_TEST_MAX_TOKENS:-1}" \
|
||||
-e MYELIN_MODEL_CACHE_DIR=/var/cache/myelin-models \
|
||||
-e MYELIN_DATASTREAM_FRAME_LOG="$FRAME_LOG" \
|
||||
${HF_TOKEN:+-e HF_TOKEN="$HF_TOKEN"} \
|
||||
"$IMAGE" >/dev/null
|
||||
|
||||
deadline=$((SECONDS + TIMEOUT_SECS))
|
||||
while (( SECONDS < deadline )); do
|
||||
logs=$(docker logs "$CONTAINER" 2>&1 || true)
|
||||
if grep -q '"type":"ready"' <<<"$logs" && grep -q '"type":"self_test_completed"' <<<"$logs"; then
|
||||
frames=$(docker exec "$CONTAINER" cat "$FRAME_LOG" 2>/dev/null || true)
|
||||
if grep -q '"channel":"myelin.node.ready"' <<<"$frames" &&
|
||||
grep -q '"channel":"myelin.worker.weights"' <<<"$frames" &&
|
||||
grep -q '"channel":"myelin.worker.prompt"' <<<"$frames"; then
|
||||
printf '%s\n' "$logs"
|
||||
printf '%s\n' "$frames"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
if grep -q 'WorkerFatal\|myelin-node: .*failed\|ModelLoadFailed\|GgufDownloadFailed' <<<"$logs"; then
|
||||
printf '%s\n' "$logs" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
docker logs "$CONTAINER" 2>&1 || true
|
||||
echo "myelin-node Docker E2E timed out after ${TIMEOUT_SECS}s" >&2
|
||||
exit 1
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
if ! mkdir /run/myelin_entrypoint.lock 2>/dev/null; then
|
||||
echo "myelin-entrypoint: already running (lock held); parking" >&2
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||
: > /root/.ssh/authorized_keys
|
||||
if [ -n "${PUBLIC_KEY:-}" ]; then printf '%s\n' "$PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
|
||||
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then printf '%s\n' "$SSH_PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
|
||||
if [ -f /etc/myelin_deploy_key.pub ]; then cat /etc/myelin_deploy_key.pub >> /root/.ssh/authorized_keys; fi
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
|
||||
if [ ! -s /root/.ssh/authorized_keys ]; then
|
||||
echo "myelin-entrypoint: WARNING no SSH public key found (PUBLIC_KEY/SSH_PUBLIC_KEY unset, no baked key); SSH will reject logins" >&2
|
||||
fi
|
||||
|
||||
mkdir -p /run/sshd /var/log /var/cache/myelin-models
|
||||
ssh-keygen -A 2>/dev/null || true
|
||||
/usr/sbin/sshd -e
|
||||
echo "myelin-entrypoint: sshd up on :22" >&2
|
||||
|
||||
NODE_LOG=/var/log/myelin-node.log
|
||||
echo "myelin-entrypoint: launching myelin-node (log -> $NODE_LOG)" >&2
|
||||
set -o pipefail
|
||||
/usr/local/bin/myelin-node "$@" 2>&1 | tee "$NODE_LOG"
|
||||
code=${PIPESTATUS[0]}
|
||||
|
||||
echo "myelin-entrypoint: myelin-node exited with code $code; NOT restarting (node held for postmortem)" >&2
|
||||
echo "myelin-entrypoint: --- last 80 lines of $NODE_LOG ---" >&2
|
||||
tail -n 80 "$NODE_LOG" >&2 || true
|
||||
exec sleep infinity
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fn main() -> std::process::ExitCode {
|
||||
myelin::run_chat_from_args(std::env::args().skip(1))
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
//! Myelin operator chat wrapper public surface.
|
||||
|
||||
mod node_image;
|
||||
pub(super) mod runtime;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
//! Myelin prompt protocol public surface.
|
||||
|
||||
pub(crate) mod rpc;
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
//! Test-only constructors and harness type aliases for the `tests` tree.
|
||||
//!
|
||||
//! These helpers previously lived in the production modules
|
||||
//! (`orchestration/run_fsm`, `staging::control`) but serve only test code.
|
||||
//! Defining them here keeps them compiled under `#[cfg(test)]` only, so
|
||||
//! production builds stay free of the dead-code warnings they would otherwise
|
||||
//! trigger. Inherent methods resolve through their type, so existing call sites
|
||||
//! such as `fsm::RunPlan::test_linear(..)` and
|
||||
//! `stage::WeightSource::embedded_gguf(..)` work unchanged.
|
||||
|
||||
use crate::run_fsm::{NodeId as FsmNodeId, OrchestratorRun, RunId as FsmRunId, RunPlan, StageRef};
|
||||
use crate::run_plan::{GgufSource, TokenizerSource};
|
||||
use crate::staging::{DeviceHandle, StageController, WeightSource};
|
||||
|
||||
impl RunPlan {
|
||||
/// Build a linear pipeline plan (stage 0 -> stage 1 -> ... -> last stage).
|
||||
/// Test-only convenience over the public `RunPlan` fields.
|
||||
pub(crate) fn test_linear(run_id: FsmRunId, stages: Vec<StageRef>) -> Self {
|
||||
Self { run_id, stages }
|
||||
}
|
||||
|
||||
/// Node ids in declared stage order.
|
||||
pub(crate) fn stage_nodes(&self) -> Vec<FsmNodeId> {
|
||||
self.stages.iter().map(|stage| stage.node_id).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceHandle {
|
||||
/// A handle on the worker's current (first) generation.
|
||||
pub(crate) fn new_current(id: u64) -> Self {
|
||||
Self { generation: 1, id }
|
||||
}
|
||||
}
|
||||
|
||||
impl WeightSource {
|
||||
/// Weights carried by an embedded GGUF file with an embedded tokenizer.
|
||||
pub(crate) fn embedded_gguf(model_id: impl Into<String>, path: impl Into<String>) -> Self {
|
||||
Self::new(
|
||||
model_id,
|
||||
GgufSource::LocalPath(path.into()),
|
||||
TokenizerSource::EmbeddedGguf,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Test alias for the orchestrator run core, retained for readable test prose.
|
||||
pub(crate) type OrchestratorHarness = OrchestratorRun;
|
||||
|
||||
/// Test alias for the stage controller core, retained for readable test prose.
|
||||
pub(crate) type StageControllerHarness = StageController;
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
name = "python"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lib]
|
||||
name = "swactor"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use ::swactor::actor::{
|
|||
Actor, ActorAddress, ActorInterface, AnyActor, Ctx, Environment, SpawnRequest,
|
||||
};
|
||||
use ::swactor::config::RuntimeConfig;
|
||||
use ::swactor::runtime::{Inbox, Runtime};
|
||||
use ::swactor::runtime::{Inbox, Runtime, RuntimeHandle};
|
||||
|
||||
// ─── PyMsg newtype ───────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -220,6 +220,8 @@ impl PyInbox {
|
|||
#[pyclass(name = "RuntimeConfig")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyRuntimeConfig {
|
||||
#[pyo3(get, set)]
|
||||
num_threads: usize,
|
||||
#[pyo3(get, set)]
|
||||
max_actors: usize,
|
||||
#[pyo3(get, set)]
|
||||
|
|
@ -231,11 +233,13 @@ impl PyRuntimeConfig {
|
|||
#[new]
|
||||
#[pyo3(signature = (
|
||||
*,
|
||||
num_threads = 1,
|
||||
max_actors = 1_000,
|
||||
channel_buffer_size = 1_000,
|
||||
))]
|
||||
fn new(max_actors: usize, channel_buffer_size: usize) -> Self {
|
||||
fn new(num_threads: usize, max_actors: usize, channel_buffer_size: usize) -> Self {
|
||||
Self {
|
||||
num_threads,
|
||||
max_actors,
|
||||
channel_buffer_size,
|
||||
}
|
||||
|
|
@ -245,6 +249,7 @@ impl PyRuntimeConfig {
|
|||
impl From<PyRuntimeConfig> for RuntimeConfig {
|
||||
fn from(py: PyRuntimeConfig) -> Self {
|
||||
RuntimeConfig {
|
||||
num_threads: py.num_threads,
|
||||
max_actors: py.max_actors,
|
||||
channel_buffer_size: py.channel_buffer_size,
|
||||
..Default::default()
|
||||
|
|
@ -275,7 +280,7 @@ impl PyRuntime {
|
|||
|
||||
fn spawn(&self, handler: PyObject) -> PyResult<PyActorAddress> {
|
||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available")
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
||||
})?;
|
||||
let actor = PyActor::new(handler);
|
||||
let addr = rt.spawn(actor).map_err(to_py_err)?;
|
||||
|
|
@ -284,14 +289,14 @@ impl PyRuntime {
|
|||
|
||||
fn send(&self, addr: &PyActorAddress, msg: PyObject) -> PyResult<()> {
|
||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available")
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
||||
})?;
|
||||
rt.send_to(addr.inner, PyMsg(msg)).map_err(to_py_err)
|
||||
}
|
||||
|
||||
fn inbox(&self) -> PyResult<PyInbox> {
|
||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available")
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
||||
})?;
|
||||
let inbox: Inbox<PyMsg> = rt.new_inbox().map_err(to_py_err)?;
|
||||
Ok(PyInbox { inner: inbox })
|
||||
|
|
@ -299,20 +304,97 @@ impl PyRuntime {
|
|||
|
||||
fn tick(&self) -> PyResult<()> {
|
||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available")
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
||||
})?;
|
||||
rt.tick();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(&mut self, py: Python<'_>) -> PyResult<PyRuntimeHandle> {
|
||||
let rt = self.inner.take().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
||||
})?;
|
||||
let handle = py.allow_threads(|| rt.run().map_err(to_py_err))?;
|
||||
Ok(PyRuntimeHandle {
|
||||
inner: Some(handle),
|
||||
})
|
||||
}
|
||||
|
||||
fn stats(&self) -> PyResult<PyRuntimeStats> {
|
||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available")
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
||||
})?;
|
||||
Ok(build_stats(rt))
|
||||
}
|
||||
|
||||
fn shutdown(&self) -> PyResult<()> {
|
||||
let rt = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
|
||||
})?;
|
||||
rt.shutdown();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ─── PyRuntimeHandle ─────────────────────────────────────────────────────────
|
||||
|
||||
#[pyclass(name = "RuntimeHandle")]
|
||||
pub struct PyRuntimeHandle {
|
||||
inner: Option<RuntimeHandle>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRuntimeHandle {
|
||||
fn spawn(&self, handler: PyObject) -> PyResult<PyActorAddress> {
|
||||
let handle = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
||||
})?;
|
||||
let actor = PyActor::new(handler);
|
||||
let addr = handle.runtime.spawn(actor).map_err(to_py_err)?;
|
||||
Ok(PyActorAddress::from(addr))
|
||||
}
|
||||
|
||||
fn send(&self, addr: &PyActorAddress, msg: PyObject) -> PyResult<()> {
|
||||
let handle = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
||||
})?;
|
||||
handle
|
||||
.runtime
|
||||
.send_to(addr.inner, PyMsg(msg))
|
||||
.map_err(to_py_err)
|
||||
}
|
||||
|
||||
fn inbox(&self) -> PyResult<PyInbox> {
|
||||
let handle = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
||||
})?;
|
||||
let inbox: Inbox<PyMsg> = handle.runtime.new_inbox().map_err(to_py_err)?;
|
||||
Ok(PyInbox { inner: inbox })
|
||||
}
|
||||
|
||||
fn stats(&self) -> PyResult<PyRuntimeStats> {
|
||||
let handle = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
||||
})?;
|
||||
Ok(build_stats(&handle.runtime))
|
||||
}
|
||||
|
||||
fn shutdown(&self) -> PyResult<()> {
|
||||
let handle = self.inner.as_ref().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
||||
})?;
|
||||
handle.shutdown();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn join(&mut self, py: Python<'_>) -> PyResult<()> {
|
||||
let handle = self.inner.take().ok_or_else(|| {
|
||||
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
|
||||
})?;
|
||||
py.allow_threads(|| handle.join());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ActorInfo / RuntimeStats ────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -436,6 +518,7 @@ fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||
m.add_class::<PyInbox>()?;
|
||||
m.add_class::<PyRuntimeConfig>()?;
|
||||
m.add_class::<PyRuntime>()?;
|
||||
m.add_class::<PyRuntimeHandle>()?;
|
||||
m.add_class::<PyActorInfo>()?;
|
||||
m.add_class::<PyWorkerInfo>()?;
|
||||
m.add_class::<PyRuntimeStats>()?;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
name = "wasm-crypto"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
name = "wasm-runtime"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
|
|
|||
|
|
@ -103,7 +103,10 @@ pub struct WasmRuntime {
|
|||
impl WasmRuntime {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Self {
|
||||
let rt = Runtime::new(RuntimeConfig::default())
|
||||
let rt = Runtime::new(RuntimeConfig {
|
||||
num_threads: 1,
|
||||
..RuntimeConfig::default()
|
||||
})
|
||||
.with_extension(Arc::new(StdExtension::new()));
|
||||
Self { rt }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ Keep this crate read-only with respect to observed programs.
|
|||
- It must not send control signals to observed runtimes.
|
||||
- It must not require changes outside `crates/dashboard` for dashboard-only work.
|
||||
|
||||
Main built-in views: `/view/swactor/workers` (worker-centric) and `/view/swactor/actor-overview` + `/view/swactor/actor-dossier` (actor-centric), all backed by `runtime.stats`, `runtime.workers`, and `runtime.actors` frames when present. Actor views are pure frame consumers and tolerant of publisher shape.
|
||||
Main built-in view: `/view/swactor/workers`, backed by `runtime.stats`, `runtime.workers`, and `runtime.actors` frames when present.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
name = "dashboard"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8"
|
||||
|
|
@ -11,5 +10,5 @@ swactor = { path = "../..", features = ["serde"] }
|
|||
parking_lot = "0.12"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio.workspace = true
|
||||
tokio = { version = "1", features = ["net", "rt-multi-thread", "sync"] }
|
||||
tokio-stream = "0.1"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Read-only HTML/SSE dashboard over incoming datastream frames.
|
||||
|
||||
The crate owns the Axum server, bounded raw frame window, and view registry. Component crates can keep their own view implementations beside their code and register them through `DashboardHandle::register_view`. The built-in swactor views are hosted here because worker/actor/message processing is universal to swactor programs: the worker page, and the actor overview (fused roster) plus per-actor dossier.
|
||||
The crate owns the Axum server, bounded raw frame window, and view registry. Component crates can keep their own view implementations beside their code and register them through `DashboardHandle::register_view`. The built-in swactor worker page is hosted here because worker/actor/message processing is universal to swactor programs.
|
||||
|
||||
## Routes
|
||||
|
||||
|
|
@ -16,9 +16,5 @@ The crate owns the Axum server, bounded raw frame window, and view registry. Com
|
|||
- `GET /api/view/fleet` — fleet and machine telemetry JSON snapshot
|
||||
- `GET /view/swactor/workers` — built-in worker page
|
||||
- `GET /api/view/swactor/workers` — worker page JSON snapshot
|
||||
- `GET /view/swactor/actor-overview` — built-in actor overview + roster page
|
||||
- `GET /api/view/swactor/actor-overview` — actor overview JSON snapshot
|
||||
- `GET /view/swactor/actor-dossier` — built-in per-actor dossier page
|
||||
- `GET /api/view/swactor/actor-dossier` — actor dossier JSON snapshot
|
||||
|
||||
All state is derived from observed frames. The dashboard sends no control signals back to producers.
|
||||
|
|
|
|||
412
crates/dashboard/src/bin/swactor_dummy_node.rs
Normal file
412
crates/dashboard/src/bin/swactor_dummy_node.rs
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use dashboard::swactor::{RUNTIME_ACTORS, RUNTIME_STATS, RUNTIME_WORKERS};
|
||||
use dashboard::{DashboardConfig, DashboardHandle, FrameEvent, StreamEvent, start_dashboard};
|
||||
use datastream::frame::{ChannelId, Frame, Lifetime, NodeId, Position, StreamId};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use swactor::actor::{ActorAddress, ActorInterface, Ctx};
|
||||
use swactor::config::RuntimeConfig;
|
||||
use swactor::runtime::Runtime;
|
||||
use swactor::stats::{ActorSnapshot, StatsHook};
|
||||
|
||||
const NODE_ID: &str = "dashboard-swactor-dummy";
|
||||
const WORKER_ACTORS: usize = 12;
|
||||
const PUBLISH_INTERVAL: Duration = Duration::from_millis(250);
|
||||
const PULSE_INTERVAL: Duration = Duration::from_millis(25);
|
||||
const WORK_ITEM_DELAY: Duration = Duration::from_micros(200);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PulseTick {
|
||||
seq: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WorkItem {
|
||||
seq: u64,
|
||||
route: u32,
|
||||
hops_left: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum RouterMsg {
|
||||
Configure { workers: Vec<ActorAddress> },
|
||||
Beat { seq: u64 },
|
||||
Complete { worker: u32, seq: u64, route: u32 },
|
||||
}
|
||||
|
||||
struct PulseActor {
|
||||
router: ActorAddress,
|
||||
}
|
||||
|
||||
impl ActorInterface for PulseActor {
|
||||
type Incoming = PulseTick;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, ctx: &Ctx, msg: PulseTick) {
|
||||
let _ = ctx.send(self.router, RouterMsg::Beat { seq: msg.seq });
|
||||
}
|
||||
}
|
||||
|
||||
struct RouterActor {
|
||||
workers: Vec<ActorAddress>,
|
||||
next: usize,
|
||||
completed: u64,
|
||||
}
|
||||
|
||||
impl RouterActor {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
workers: Vec::new(),
|
||||
next: 0,
|
||||
completed: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActorInterface for RouterActor {
|
||||
type Incoming = RouterMsg;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, ctx: &Ctx, msg: RouterMsg) {
|
||||
match msg {
|
||||
RouterMsg::Configure { workers } => {
|
||||
self.workers = workers;
|
||||
self.next = 0;
|
||||
}
|
||||
RouterMsg::Beat { seq } => {
|
||||
if self.workers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let burst = 48 + (seq as usize % 32);
|
||||
for route in 0..burst {
|
||||
let target = self.workers[self.next % self.workers.len()];
|
||||
self.next = self.next.wrapping_add(1);
|
||||
let _ = ctx.send(
|
||||
target,
|
||||
WorkItem {
|
||||
seq,
|
||||
route: route as u32,
|
||||
hops_left: 1 + ((seq + route as u64) % 3) as u8,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
RouterMsg::Complete { worker, seq, route } => {
|
||||
self.completed = self.completed.wrapping_add(1);
|
||||
|
||||
if self.completed % 7 == 0 && !self.workers.is_empty() {
|
||||
let target =
|
||||
self.workers[(worker as usize + route as usize) % self.workers.len()];
|
||||
let _ = ctx.send(
|
||||
target,
|
||||
WorkItem {
|
||||
seq,
|
||||
route: route.wrapping_add(1000),
|
||||
hops_left: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WorkerActor {
|
||||
id: u32,
|
||||
router: ActorAddress,
|
||||
}
|
||||
|
||||
impl ActorInterface for WorkerActor {
|
||||
type Incoming = WorkItem;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, ctx: &Ctx, msg: WorkItem) {
|
||||
if msg.hops_left > 0 {
|
||||
let _ = ctx.send(
|
||||
ctx.self_addr(),
|
||||
WorkItem {
|
||||
seq: msg.seq,
|
||||
route: msg.route,
|
||||
hops_left: msg.hops_left - 1,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
thread::sleep(WORK_ITEM_DELAY);
|
||||
let _ = ctx.send(
|
||||
self.router,
|
||||
RouterMsg::Complete {
|
||||
worker: self.id,
|
||||
seq: msg.seq,
|
||||
route: msg.route,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct QueuedSinkActor;
|
||||
|
||||
impl ActorInterface for QueuedSinkActor {
|
||||
type Incoming = WorkItem;
|
||||
type Response = ();
|
||||
|
||||
fn handle(&mut self, ctx: &Ctx, _msg: WorkItem) {
|
||||
ctx.suspend_self();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DashboardStatsHook {
|
||||
actors: Mutex<HashMap<ActorAddress, ActorDetail>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ActorDetail {
|
||||
worker_id: usize,
|
||||
mailbox_depth: usize,
|
||||
last_msg_type: Option<String>,
|
||||
messages_processed: u64,
|
||||
poisoned: bool,
|
||||
message_type_counts: Vec<(String, u64)>,
|
||||
}
|
||||
|
||||
impl StatsHook for DashboardStatsHook {
|
||||
fn on_tick(&self, worker_id: usize, snapshots: &[ActorSnapshot]) {
|
||||
let mut actors = self.actors.lock();
|
||||
for snapshot in snapshots {
|
||||
actors.insert(
|
||||
snapshot.address,
|
||||
ActorDetail {
|
||||
worker_id,
|
||||
mailbox_depth: snapshot.mailbox_depth,
|
||||
last_msg_type: snapshot.last_msg_type.map(str::to_owned),
|
||||
messages_processed: snapshot.messages_processed,
|
||||
poisoned: snapshot.poisoned,
|
||||
message_type_counts: snapshot
|
||||
.message_type_counts
|
||||
.iter()
|
||||
.map(|(name, count)| ((*name).to_owned(), *count))
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DashboardStatsHook {
|
||||
fn snapshot(
|
||||
&self,
|
||||
live_workers: &[(ActorAddress, usize)],
|
||||
names: &HashMap<ActorAddress, String>,
|
||||
) -> Vec<ActorDetailFrame> {
|
||||
let actors = self.actors.lock();
|
||||
live_workers
|
||||
.iter()
|
||||
.map(|(address, worker_id)| {
|
||||
let detail = actors.get(address);
|
||||
ActorDetailFrame {
|
||||
address: address.to_string(),
|
||||
name: names.get(address).cloned(),
|
||||
worker_id: detail.map_or(*worker_id, |detail| detail.worker_id),
|
||||
mailbox_depth: detail.map_or(0, |detail| detail.mailbox_depth),
|
||||
last_msg_type: detail.and_then(|detail| detail.last_msg_type.clone()),
|
||||
messages_processed: detail.map_or(0, |detail| detail.messages_processed),
|
||||
poisoned: detail.is_some_and(|detail| detail.poisoned),
|
||||
message_type_counts: detail
|
||||
.map(|detail| detail.message_type_counts.clone())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ActorDetailFrame {
|
||||
address: String,
|
||||
name: Option<String>,
|
||||
worker_id: usize,
|
||||
mailbox_depth: usize,
|
||||
last_msg_type: Option<String>,
|
||||
messages_processed: u64,
|
||||
poisoned: bool,
|
||||
message_type_counts: Vec<(String, u64)>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let dashboard = start_dashboard(DashboardConfig::default());
|
||||
dashboard.start_http_standalone();
|
||||
|
||||
let mut runtime = Runtime::new(RuntimeConfig {
|
||||
num_threads: 4,
|
||||
max_actors: 128,
|
||||
channel_buffer_size: 4096,
|
||||
actor_message_budget: 8,
|
||||
});
|
||||
let stats_hook = Arc::new(DashboardStatsHook::default());
|
||||
runtime.set_stats_hook(stats_hook.clone());
|
||||
|
||||
let router = runtime.spawn(RouterActor::new()).expect("spawn router");
|
||||
let mut names = HashMap::new();
|
||||
names.insert(router, "router".to_owned());
|
||||
|
||||
let mut workers = Vec::with_capacity(WORKER_ACTORS);
|
||||
for id in 0..WORKER_ACTORS {
|
||||
let address = runtime
|
||||
.spawn(WorkerActor {
|
||||
id: id as u32,
|
||||
router,
|
||||
})
|
||||
.expect("spawn worker actor");
|
||||
names.insert(address, format!("worker-{id}"));
|
||||
workers.push(address);
|
||||
}
|
||||
|
||||
let pulse = runtime.spawn(PulseActor { router }).expect("spawn pulse");
|
||||
names.insert(pulse, "pulse".to_owned());
|
||||
let queued_sink = runtime.spawn(QueuedSinkActor).expect("spawn queued sink");
|
||||
names.insert(queued_sink, "queued-sink".to_owned());
|
||||
runtime
|
||||
.send_to(
|
||||
router,
|
||||
RouterMsg::Configure {
|
||||
workers: workers.clone(),
|
||||
},
|
||||
)
|
||||
.expect("configure router");
|
||||
|
||||
let runtime = runtime.run().expect("start swactor runtime");
|
||||
let stream = StreamId::new(NodeId::new(NODE_ID), Lifetime(1));
|
||||
let mut position = 0_u64;
|
||||
let mut seq = 0_u64;
|
||||
let mut ticks_until_publish = 0_u8;
|
||||
|
||||
println!(
|
||||
"dashboard listening at http://127.0.0.1:{}/view/swactor/workers",
|
||||
DashboardConfig::default().port
|
||||
);
|
||||
println!(
|
||||
"dummy node {NODE_ID} running {} swactor actors",
|
||||
names.len()
|
||||
);
|
||||
|
||||
loop {
|
||||
let _ = runtime.runtime.send_to(pulse, PulseTick { seq });
|
||||
if seq % 2 == 0 {
|
||||
let _ = runtime.runtime.send_to(
|
||||
queued_sink,
|
||||
WorkItem {
|
||||
seq,
|
||||
route: u32::MAX,
|
||||
hops_left: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
seq = seq.wrapping_add(1);
|
||||
|
||||
if ticks_until_publish == 0 {
|
||||
publish_runtime_snapshot(
|
||||
&dashboard,
|
||||
&stream,
|
||||
&mut position,
|
||||
&runtime.runtime,
|
||||
&stats_hook,
|
||||
&names,
|
||||
);
|
||||
ticks_until_publish = (PUBLISH_INTERVAL.as_millis() / PULSE_INTERVAL.as_millis()) as u8;
|
||||
}
|
||||
ticks_until_publish = ticks_until_publish.saturating_sub(1);
|
||||
|
||||
thread::sleep(PULSE_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn publish_runtime_snapshot(
|
||||
dashboard: &DashboardHandle,
|
||||
stream: &StreamId,
|
||||
position: &mut u64,
|
||||
runtime: &Runtime,
|
||||
stats_hook: &DashboardStatsHook,
|
||||
names: &HashMap<ActorAddress, String>,
|
||||
) {
|
||||
let stats = runtime.stats();
|
||||
let actor_details = stats_hook.snapshot(&stats.actors, names);
|
||||
let actors: Vec<Value> = stats
|
||||
.actors
|
||||
.iter()
|
||||
.map(|(address, worker_id)| json!([address.to_string(), worker_id]))
|
||||
.collect();
|
||||
let workers = serde_json::to_value(&stats.workers).expect("serialize worker stats");
|
||||
let actor_details = serde_json::to_value(actor_details).expect("serialize actor stats");
|
||||
let tick_timings = serde_json::to_value(&stats.tick_timings).expect("serialize tick timings");
|
||||
let total_mailbox_depth: usize = stats
|
||||
.workers
|
||||
.iter()
|
||||
.map(|worker| worker.mailbox_depth)
|
||||
.sum();
|
||||
|
||||
publish_json(
|
||||
dashboard,
|
||||
stream,
|
||||
position,
|
||||
RUNTIME_STATS,
|
||||
json!({
|
||||
"num_workers": stats.num_workers,
|
||||
"uptime_ms": stats.uptime_ms,
|
||||
"actors_live": stats.actors.len(),
|
||||
"mailbox_depth": total_mailbox_depth,
|
||||
"actors": actors,
|
||||
"workers": workers,
|
||||
"actor_details": actor_details,
|
||||
"tick_timings": tick_timings,
|
||||
}),
|
||||
);
|
||||
|
||||
publish_json(
|
||||
dashboard,
|
||||
stream,
|
||||
position,
|
||||
RUNTIME_WORKERS,
|
||||
json!({ "workers": stats.workers }),
|
||||
);
|
||||
|
||||
publish_json(
|
||||
dashboard,
|
||||
stream,
|
||||
position,
|
||||
RUNTIME_ACTORS,
|
||||
json!({ "actors": actor_details }),
|
||||
);
|
||||
}
|
||||
|
||||
fn publish_json(
|
||||
dashboard: &DashboardHandle,
|
||||
stream: &StreamId,
|
||||
position: &mut u64,
|
||||
channel: &str,
|
||||
value: Value,
|
||||
) {
|
||||
let payload = serde_json::to_vec(&value).expect("serialize dashboard frame");
|
||||
let frame = Frame::new(
|
||||
ChannelId(*position as u32 + 1),
|
||||
Position(*position),
|
||||
payload,
|
||||
);
|
||||
dashboard.publish(FrameEvent {
|
||||
stream: StreamEvent {
|
||||
node: stream.node.as_str().to_string(),
|
||||
life: stream.life.0,
|
||||
},
|
||||
channel: channel.to_owned(),
|
||||
position: *position,
|
||||
payload: frame.payload.clone(),
|
||||
});
|
||||
*position = position.wrapping_add(1);
|
||||
}
|
||||
|
|
@ -97,8 +97,6 @@ impl DashboardHandle {
|
|||
views.register(Arc::new(live_explorer::LiveDatastreamExplorer::default()));
|
||||
views.register(Arc::new(hardware_view::HardwareDashboardView::default()));
|
||||
views.register(swactor::worker_view());
|
||||
views.register(swactor::actor_overview_view());
|
||||
views.register(swactor::actor_dossier_view());
|
||||
let store = Arc::new(DashboardStore::new(
|
||||
config.raw_frame_history,
|
||||
Arc::clone(&views),
|
||||
|
|
|
|||
|
|
@ -1,185 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Actor dossier — swactor</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0f172a; --panel:#1e293b; --panel2:#172033; --border:#334155;
|
||||
--text:#e2e8f0; --muted:#94a3b8; --accent:#60a5fa;
|
||||
--running:#34d399; --poisoned:#f87171;
|
||||
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;padding:0;background:var(--bg);color:var(--text);}
|
||||
.mono,.table,th,td,.value,.num{font-variant-numeric:tabular-nums;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
|
||||
header.bar{display:flex;align-items:center;gap:16px;padding:14px 20px;border-bottom:1px solid var(--border);background:var(--panel);position:sticky;top:0;z-index:5;}
|
||||
header.bar h1{font-size:18px;margin:0;font-weight:600;letter-spacing:.01em;}
|
||||
nav.tabs{display:flex;gap:4px;margin-left:auto;}
|
||||
nav.tabs a{padding:6px 12px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:13px;border:1px solid transparent;}
|
||||
nav.tabs a:hover{color:var(--text);background:var(--panel2);}
|
||||
nav.tabs a.cur{color:var(--accent);background:var(--panel2);border-color:var(--border);}
|
||||
select{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;}
|
||||
.wrap{padding:20px;max-width:1100px;margin:0 auto;}
|
||||
.picker{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:18px;}
|
||||
.picker label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;}
|
||||
#pick{min-width:300px;max-width:560px;flex:1;}
|
||||
.back-link{font-size:13px;}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:99px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:600;}
|
||||
.empty{padding:28px;border:1px dashed #475569;border-radius:12px;color:var(--muted);text-align:center;}
|
||||
.toast{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);background:var(--panel2);border:1px solid var(--border);color:var(--text);padding:10px 16px;border-radius:10px;font-size:13px;max-width:min(90vw,560px);text-align:center;z-index:50;box-shadow:0 8px 24px rgba(0,0,0,.45);}
|
||||
.toast.err{border-color:var(--poisoned);color:var(--poisoned);}
|
||||
.kvs{display:grid;grid-template-columns:max-content 1fr;gap:6px 14px;}
|
||||
.kvs .k{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.05em;}
|
||||
.kvs .v{font-size:13px;word-break:break-word;}
|
||||
.section{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px 18px;margin-bottom:16px;}
|
||||
h2{font-size:14px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:0 0 12px;}
|
||||
.mb-dyn{display:flex;gap:24px;align-items:center;flex-wrap:wrap;}
|
||||
.mb-spark{flex:1 1 320px;min-width:0;}
|
||||
.mb-spark .spark{display:block;width:100%;height:120px;}
|
||||
.mb-vals{display:flex;flex-direction:column;gap:16px;min-width:150px;}
|
||||
.mb-val .label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em;}
|
||||
.mb-val .value{font-size:28px;margin-top:4px;font-weight:600;line-height:1;}
|
||||
.bar-row{display:flex;align-items:center;gap:8px;margin:5px 0;}
|
||||
.bar-row .name{width:48%;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.bar-track{flex:1;height:10px;background:var(--panel2);border-radius:99px;overflow:hidden;}
|
||||
.bar-fill{height:100%;background:var(--accent);border-radius:99px;}
|
||||
.bar-row .num{width:90px;text-align:right;color:var(--muted);}
|
||||
.bar-row.last .bar-fill{background:var(--running);}
|
||||
.bar-row.last{font-weight:600;}
|
||||
a{color:var(--accent);text-decoration:none;}
|
||||
.stale{opacity:.55;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="bar">
|
||||
<h1>Actor dossier</h1>
|
||||
<select id="runtime"></select>
|
||||
<nav class="tabs">
|
||||
<a href="/view/swactor/actor-overview">Overview</a>
|
||||
<a href="/view/swactor/actor-dossier" class="cur">Dossier</a>
|
||||
<a href="/view/swactor/workers">Workers</a>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="wrap">
|
||||
<div class="picker">
|
||||
<label for="pick">Actor</label>
|
||||
<select id="pick"></select>
|
||||
<a class="back-link" href="/view/swactor/actor-overview">← back to overview</a>
|
||||
</div>
|
||||
<div id="empty" class="toast" hidden>No runtime actor frames yet.</div>
|
||||
<div id="content"></div>
|
||||
</div>
|
||||
<script>
|
||||
const POLL_MS=750;
|
||||
const STATE_COLOR={running:'#34d399',poisoned:'#f87171'};
|
||||
const elRuntime=document.getElementById('runtime');
|
||||
const elPick=document.getElementById('pick');
|
||||
const elEmpty=document.getElementById('empty');
|
||||
const elContent=document.getElementById('content');
|
||||
let runtimes=[],runtimeKey='',selectedAddr='';
|
||||
|
||||
function fmt(n,d=0){if(n==null||n===undefined)return'\u2014';if(typeof n!=='number')return String(n);return n.toLocaleString(undefined,{maximumFractionDigits:d});}
|
||||
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c];});}
|
||||
function shortAddr(a){a=String(a||'');return a.length<=12?a:a.slice(0,6)+'\u2026'+a.slice(-4);}
|
||||
function urlActor(){try{return new URLSearchParams(location.search).get('actor');}catch(e){return null;}}
|
||||
function stateBadge(a){const s=a.poisoned?'poisoned':'running';const c=STATE_COLOR[s];return '<span class="badge" style="color:'+c+';background:'+c+'22">'+s+'</span>';}
|
||||
function growthColor(g){if(g>1.5)return '#f87171';if(g>0.3)return '#fbbf24';return '#34d399';}
|
||||
function areaChart(data,w,h,color){
|
||||
if(!data||data.length<2)return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="100%" height="'+h+'" preserveAspectRatio="none"></svg>';
|
||||
const max=Math.max(1,...data);const n=data.length;
|
||||
let line='',area='0,'+h+' ';
|
||||
for(let i=0;i<n;i++){const x=(i/(n-1)*w).toFixed(1);const y=(h-(data[i]/max)*(h-2)-1).toFixed(1);line+=x+','+y+' ';area+=x+','+y+' ';}
|
||||
area+=' '+w+','+h;
|
||||
return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="100%" height="'+h+'" preserveAspectRatio="none">'+
|
||||
'<polygon points="'+area.trim()+'" fill="'+(color||'#60a5fa')+'22"/>'+
|
||||
'<polyline points="'+line.trim()+'" fill="none" stroke="'+(color||'#60a5fa')+'" stroke-width="1.5"/></svg>';
|
||||
}
|
||||
function kv(k,vhtml){return '<div class="k">'+esc(k)+'</div><div class="v">'+vhtml+'</div>';}
|
||||
function typeShort(t){t=String(t||'');const i=t.lastIndexOf('::');return i>=0?t.slice(i+2):t;}
|
||||
function pickRuntime(list){
|
||||
if(!runtimeKey||!list.some(r=>r.stream.key===runtimeKey)){
|
||||
const live=list.find(r=>r.live)||list[0];runtimeKey=live?live.stream.key:'';
|
||||
}
|
||||
return list.find(r=>r.stream.key===runtimeKey)||null;
|
||||
}
|
||||
function currentActors(rt){return rt?(rt.actors||[]):[];}
|
||||
function findActor(rt){
|
||||
const actors=currentActors(rt);
|
||||
if(selectedAddr){const hit=actors.find(a=>a.address===selectedAddr);if(hit)return hit;}
|
||||
return actors[0]||null;
|
||||
}
|
||||
function renderPicker(rt){
|
||||
const actors=currentActors(rt);
|
||||
elPick.innerHTML=actors.map(a=>
|
||||
'<option value="'+esc(a.address)+'">'+esc(shortAddr(a.address))+(a.name?' \u00b7 '+esc(a.name):'')+'</option>'
|
||||
).join('');
|
||||
const cur=findActor(rt);
|
||||
if(cur){selectedAddr=cur.address;elPick.value=selectedAddr;}
|
||||
}
|
||||
function renderRuntimeSelector(){
|
||||
elRuntime.innerHTML=runtimes.map(r=>
|
||||
'<option value="'+esc(r.stream.key)+'">'+esc(r.stream.node)+'#'+r.stream.life+(r.live?'':' (stale)')+'</option>'
|
||||
).join('');
|
||||
if(runtimeKey)elRuntime.value=runtimeKey;
|
||||
}
|
||||
function render(a,rt){
|
||||
if(!a){elContent.innerHTML='<div class="empty">Select an actor. None are present on the current frame.</div>';return;}
|
||||
const lastType=a.last_msg_type||'\u2014';
|
||||
const diet=(a.message_type_counts||[]).slice();
|
||||
diet.sort((x,y)=>y.count-x.count);
|
||||
const dmax=Math.max(1,...diet.map(d=>d.count));
|
||||
const lastName=a.last_msg_type;
|
||||
const dietHtml=diet.length?diet.map(d=>{
|
||||
const cls=d.message_type===lastName?'bar-row last':'bar-row';
|
||||
return '<div class="'+cls+'"><span class="name">'+esc(d.message_type)+'</span>'+
|
||||
'<div class="bar-track"><div class="bar-fill" style="width:'+(d.count/dmax*100).toFixed(1)+'%"></div></div>'+
|
||||
'<span class="num">'+fmt(d.count)+'</span></div>';
|
||||
}).join(''):'<div class="empty">no message-type counts on frame</div>';
|
||||
const hist=(a.history||[]).map(h=>h.mailbox_depth);
|
||||
const g=a.mailbox_growth||0;
|
||||
elContent.innerHTML=
|
||||
'<div class="section"><h2>Identity & lifecycle</h2><div class="kvs">'+
|
||||
kv('address','<span class="mono">'+esc(a.address)+'</span>')+
|
||||
(a.name?kv('name',esc(a.name)):'')+
|
||||
(a.actor_type?kv('actor type','<span class="mono" title="'+esc(a.actor_type)+'">'+esc(typeShort(a.actor_type))+'</span>'):'')+
|
||||
(a.message_type?kv('message type','<span class="mono" title="'+esc(a.message_type)+'">'+esc(typeShort(a.message_type))+'</span>'):'')+
|
||||
kv('state',stateBadge(a))+
|
||||
kv('worker',a.worker_id==null?'\u2014':esc(a.worker_id))+
|
||||
kv('last message',esc(lastType))+
|
||||
kv('last seen',a.last_seen_ms_ago!=null?(a.last_seen_ms_ago/1000).toFixed(1)+'s ago':'\u2014')+
|
||||
'</div></div>'+
|
||||
'<div class="section"><h2>Mailbox dynamics</h2><div class="mb-dyn">'+
|
||||
'<div class="mb-spark">'+areaChart(hist,720,120,growthColor(g))+'</div>'+
|
||||
'<div class="mb-vals">'+
|
||||
'<div class="mb-val"><div class="label">depth</div><div class="value" style="color:'+growthColor(g)+'">'+fmt(a.mailbox_depth)+'</div></div>'+
|
||||
'<div class="mb-val"><div class="label">growth</div><div class="value">'+fmt(g,1)+'/s</div></div>'+
|
||||
'<div class="mb-val"><div class="label">msg/s</div><div class="value">'+fmt(a.msg_per_sec,1)+'</div></div>'+
|
||||
'</div>'+
|
||||
'</div></div>'+
|
||||
'<div class="section"><h2>Message diet</h2>'+dietHtml+'</div>';
|
||||
}
|
||||
async function refresh(){
|
||||
try{
|
||||
const res=await fetch('/api/view/swactor/actor-dossier',{cache:'no-store'});
|
||||
if(!res.ok)throw new Error('HTTP '+res.status);
|
||||
const data=await res.json();
|
||||
runtimes=data.runtimes||[];
|
||||
renderRuntimeSelector();
|
||||
const rt=pickRuntime(runtimes);
|
||||
if(rt&¤tActors(rt).length>0){elEmpty.hidden=true;}else{elEmpty.className='toast';elEmpty.textContent='No runtime actor frames yet.';elEmpty.hidden=false;}
|
||||
renderPicker(rt);
|
||||
render(findActor(rt),rt);
|
||||
}catch(e){
|
||||
elEmpty.className='toast err';elEmpty.hidden=false;elEmpty.textContent='Failed to load actor frames: '+e.message;elContent.innerHTML='';
|
||||
}
|
||||
}
|
||||
elRuntime.addEventListener('change',e=>{runtimeKey=e.target.value;const rt=pickRuntime(runtimes);renderPicker(rt);render(findActor(rt),rt);});
|
||||
elPick.addEventListener('change',e=>{selectedAddr=e.target.value;const rt=pickRuntime(runtimes);render(findActor(rt),rt);});
|
||||
const q=urlActor();if(q)selectedAddr=q;
|
||||
refresh();
|
||||
setInterval(refresh,POLL_MS);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Actor overview — swactor</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0f172a; --panel:#1e293b; --panel2:#172033; --border:#334155;
|
||||
--text:#e2e8f0; --muted:#94a3b8; --accent:#60a5fa;
|
||||
--running:#34d399; --poisoned:#f87171;
|
||||
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;padding:0;background:var(--bg);color:var(--text);}
|
||||
.mono,.table,th,td,.value,.num{font-variant-numeric:tabular-nums;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
|
||||
header.bar{display:flex;align-items:center;gap:16px;padding:14px 20px;border-bottom:1px solid var(--border);background:var(--panel);position:sticky;top:0;z-index:5;}
|
||||
header.bar h1{font-size:18px;margin:0;font-weight:600;letter-spacing:.01em;}
|
||||
nav.tabs{display:flex;gap:4px;margin-left:auto;}
|
||||
nav.tabs a{padding:6px 12px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:13px;border:1px solid transparent;}
|
||||
nav.tabs a:hover{color:var(--text);background:var(--panel2);}
|
||||
nav.tabs a.cur{color:var(--accent);background:var(--panel2);border-color:var(--border);}
|
||||
select{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;}
|
||||
.wrap{padding:20px;max-width:1400px;margin:0 auto;}
|
||||
.grid{display:grid;gap:12px;}
|
||||
.cards{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));margin-bottom:16px;}
|
||||
.card{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:14px;}
|
||||
.card .label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em;}
|
||||
.card .value{font-size:26px;margin-top:6px;font-weight:600;}
|
||||
.card .value.bad{color:var(--poisoned);}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:99px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:600;}
|
||||
.table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden;}
|
||||
.table th,.table td{padding:8px 10px;border-bottom:1px solid #1f2937;text-align:left;white-space:nowrap;}
|
||||
.table th{color:#93c5fd;background:var(--panel2);cursor:pointer;user-select:none;font-size:12px;text-transform:uppercase;letter-spacing:.05em;}
|
||||
.table th:hover{color:#fff;}
|
||||
.table tr{cursor:pointer;}
|
||||
.table tbody tr:hover{background:#1d4ed822;}
|
||||
.table td.muted,.muted{color:var(--muted);}
|
||||
.spark{display:block;}
|
||||
.bar-row{display:flex;align-items:center;gap:8px;margin:5px 0;}
|
||||
.bar-row .name{width:38%;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.bar-track{flex:1;height:10px;background:var(--panel2);border-radius:99px;overflow:hidden;}
|
||||
.bar-fill{height:100%;background:var(--accent);border-radius:99px;}
|
||||
.bar-row .num{width:90px;text-align:right;color:var(--muted);}
|
||||
input{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;}
|
||||
.filters{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px;}
|
||||
.filters label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;}
|
||||
h2{font-size:14px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:22px 0 10px;}
|
||||
.empty{padding:28px;border:1px dashed #475569;border-radius:12px;color:var(--muted);text-align:center;}
|
||||
.toast{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);background:var(--panel2);border:1px solid var(--border);color:var(--text);padding:10px 16px;border-radius:10px;font-size:13px;max-width:min(90vw,560px);text-align:center;z-index:50;box-shadow:0 8px 24px rgba(0,0,0,.45);}
|
||||
.toast.err{border-color:var(--poisoned);color:var(--poisoned);}
|
||||
.stacked{display:flex;height:18px;border-radius:6px;overflow:hidden;border:1px solid var(--border);}
|
||||
.legend{display:flex;gap:14px;flex-wrap:wrap;margin-top:8px;font-size:12px;color:var(--muted);}
|
||||
.legend i{display:inline-block;width:12px;height:12px;margin-right:5px;border-radius:3px;vertical-align:middle;}
|
||||
a{color:var(--accent);text-decoration:none;}
|
||||
.roster-box{max-height:520px;overflow:auto;border:1px solid var(--border);border-radius:12px;}
|
||||
.roster-box .table{border-radius:0;border:none;}
|
||||
.stale{opacity:.55;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="bar">
|
||||
<h1>Actor overview</h1>
|
||||
<select id="runtime"></select>
|
||||
<nav class="tabs">
|
||||
<a href="/view/swactor/actor-overview" class="cur">Overview</a>
|
||||
<a href="/view/swactor/actor-dossier">Dossier</a>
|
||||
<a href="/view/swactor/workers">Workers</a>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="wrap">
|
||||
<div id="empty" class="toast" hidden>No runtime actor frames yet. A live swactor runtime publishing <code>runtime.actors</code> will populate this page.</div>
|
||||
<div id="cards" class="grid cards"></div>
|
||||
|
||||
<h2>State distribution</h2>
|
||||
<div class="stacked" id="dist"></div>
|
||||
<div class="legend" id="distLegend"></div>
|
||||
|
||||
<h2>Throughput</h2>
|
||||
<div id="throughput"></div>
|
||||
|
||||
<h2>Worker balance</h2>
|
||||
<div id="workers"></div>
|
||||
|
||||
<h2>Hot actors — mailbox growth</h2>
|
||||
<div id="hot"></div>
|
||||
|
||||
<h2>Alerts</h2>
|
||||
<div id="alerts"></div>
|
||||
|
||||
<h2>Roster</h2>
|
||||
<div class="filters">
|
||||
<label for="f-search">search</label>
|
||||
<input id="f-search" type="search" placeholder="name · addr · worker" autocomplete="off" spellcheck="false">
|
||||
<label for="f-state">state</label>
|
||||
<select id="f-state">
|
||||
<option value="all">all states</option>
|
||||
<option value="running">running</option>
|
||||
<option value="poisoned">poisoned</option>
|
||||
</select>
|
||||
<label for="f-worker">worker</label>
|
||||
<select id="f-worker"></select>
|
||||
</div>
|
||||
<div class="roster-box">
|
||||
<table class="table">
|
||||
<thead><tr id="head"></tr></thead>
|
||||
<tbody id="rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const POLL_MS=750;
|
||||
const elRuntime=document.getElementById('runtime');
|
||||
const elEmpty=document.getElementById('empty');
|
||||
const elCards=document.getElementById('cards');
|
||||
const elDist=document.getElementById('dist');
|
||||
const elDistLegend=document.getElementById('distLegend');
|
||||
const elThroughput=document.getElementById('throughput');
|
||||
const elWorkers=document.getElementById('workers');
|
||||
const elHot=document.getElementById('hot');
|
||||
const elAlerts=document.getElementById('alerts');
|
||||
const elHead=document.getElementById('head');
|
||||
const elRows=document.getElementById('rows');
|
||||
const elSearch=document.getElementById('f-search');
|
||||
const elState=document.getElementById('f-state');
|
||||
const elWorker=document.getElementById('f-worker');
|
||||
|
||||
const STATE_COLOR={running:'#34d399',poisoned:'#f87171'};
|
||||
const COLS=[['actor','Actor'],['type','Type'],['state','State'],['mailbox','Mailbox'],['rate','Msg/s'],
|
||||
['processed','Processed'],['last','Last msg'],['worker','Worker']];
|
||||
const DEFDIR={actor:'asc',type:'asc',state:'asc',mailbox:'desc',rate:'desc',processed:'desc',last:'asc',worker:'asc'};
|
||||
let sortKey='mailbox',sortDir='desc';
|
||||
let fSearch='',fState='all',fWorker='all';
|
||||
let runtimes=[],runtimeKey='';
|
||||
|
||||
function fmt(n,d=0){if(n==null||n===undefined)return'\u2014';if(typeof n!=='number')return String(n);return n.toLocaleString(undefined,{maximumFractionDigits:d});}
|
||||
function ago(sec){sec=Math.max(0,Math.round(sec));if(sec<60)return sec+'s';const m=Math.floor(sec/60);if(m<60)return m+'m';return Math.floor(m/60)+'h';}
|
||||
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c];});}
|
||||
function shortAddr(a){a=String(a||'');return a.length<=12?a:a.slice(0,6)+'\u2026'+a.slice(-4);}
|
||||
function typeShort(t){t=String(t||'');const i=t.lastIndexOf('::');return i>=0?t.slice(i+2):t;}
|
||||
function stateBadge(a){const s=a.poisoned?'poisoned':'running';const c=STATE_COLOR[s];return '<span class="badge" style="color:'+c+';background:'+c+'22">'+s+'</span>';}
|
||||
function growthColor(g){if(g>1.5)return '#f87171';if(g>0.3)return '#fbbf24';return '#34d399';}
|
||||
function sparkline(data,w,h,color){
|
||||
if(!data||data.length<2)return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="'+w+'" height="'+h+'"></svg>';
|
||||
const max=Math.max(1,...data);const n=data.length;
|
||||
let pts='';for(let i=0;i<n;i++)pts+=(i/(n-1)*w).toFixed(1)+','+(h-(data[i]/max)*(h-2)-1).toFixed(1)+' ';
|
||||
return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="'+w+'" height="'+h+'" preserveAspectRatio="none"><polyline points="'+pts.trim()+'" fill="none" stroke="'+(color||'#60a5fa')+'" stroke-width="1.5"/></svg>';
|
||||
}
|
||||
function pickRuntime(list){
|
||||
if(!runtimeKey||!list.some(r=>r.stream.key===runtimeKey)){
|
||||
const live=list.find(r=>r.live)||list[0];
|
||||
runtimeKey=live?live.stream.key:'';
|
||||
}
|
||||
return list.find(r=>r.stream.key===runtimeKey)||null;
|
||||
}
|
||||
function renderRuntimeSelector(){
|
||||
elRuntime.innerHTML=runtimes.map(r=>
|
||||
'<option value="'+esc(r.stream.key)+'">'+esc(r.stream.node)+'#'+r.stream.life+(r.live?'':' (stale)')+'</option>'
|
||||
).join('');
|
||||
if(runtimeKey)elRuntime.value=runtimeKey;
|
||||
}
|
||||
function renderCards(rt){
|
||||
const s=rt.summary||{};
|
||||
const uptime=s.uptime_ms!=null?ago(s.uptime_ms/1000):'\u2014';
|
||||
elCards.innerHTML=
|
||||
'<div class="card"><div class="label">Actors</div><div class="value">'+fmt(s.actors)+'</div></div>'+
|
||||
'<div class="card"><div class="label">msg/s</div><div class="value">'+fmt(s.msg_per_sec,1)+'</div></div>'+
|
||||
'<div class="card"><div class="label">Mailbox</div><div class="value">'+fmt(s.mailbox_depth)+'</div></div>'+
|
||||
'<div class="card"><div class="label">Poisoned</div><div class="value'+(s.poisoned>0?' bad':'')+'">'+fmt(s.poisoned)+'</div></div>'+
|
||||
'<div class="card"><div class="label">Uptime</div><div class="value">'+uptime+'</div></div>';
|
||||
}
|
||||
function renderDist(rt){
|
||||
const actors=rt.actors||[];const n=actors.length||1;
|
||||
let running=0,poisoned=0;
|
||||
actors.forEach(a=>{if(a.poisoned)poisoned++;else running++;});
|
||||
let segs='';
|
||||
if(running)segs+='<div style="width:'+(running/n*100).toFixed(2)+'%;background:'+STATE_COLOR.running+';"></div>';
|
||||
if(poisoned)segs+='<div style="width:'+(poisoned/n*100).toFixed(2)+'%;background:'+STATE_COLOR.poisoned+';"></div>';
|
||||
elDist.innerHTML=segs||'<div style="width:100%;background:var(--panel2);"></div>';
|
||||
elDistLegend.innerHTML='<span><i style="background:'+STATE_COLOR.running+'"></i>running: '+running+'</span>'+
|
||||
'<span><i style="background:'+STATE_COLOR.poisoned+'"></i>poisoned: '+poisoned+'</span>';
|
||||
}
|
||||
function renderThroughput(rt){
|
||||
const h=(rt.history||[]).map(x=>x.msg_per_sec);
|
||||
const mb=(rt.history||[]).map(x=>x.mailbox_depth);
|
||||
elThroughput.innerHTML=
|
||||
sparkline(h,760,90,'#60a5fa')+'<div style="height:6px"></div>'+
|
||||
sparkline(mb,760,90,'#fbbf24')+
|
||||
'<div class="legend"><span><i style="background:#60a5fa"></i>msg/s</span><span><i style="background:#fbbf24"></i>mailbox</span></div>';
|
||||
}
|
||||
function renderWorkers(rt){
|
||||
const counts={};
|
||||
(rt.actors||[]).forEach(a=>{const w=a.worker_id==null?'?':a.worker_id;counts[w]=(counts[w]||0)+1;});
|
||||
const keys=Object.keys(counts).sort();
|
||||
const max=Math.max(1,...Object.values(counts));
|
||||
elWorkers.innerHTML=keys.map(w=>{
|
||||
const c=counts[w];
|
||||
return '<div class="bar-row"><span class="name">worker '+esc(w)+'</span>'+
|
||||
'<div class="bar-track"><div class="bar-fill" style="width:'+(c/max*100).toFixed(1)+'%"></div></div>'+
|
||||
'<span class="num">'+c+'</span></div>';
|
||||
}).join('')||'<div class="empty">no worker placement on frame</div>';
|
||||
}
|
||||
function renderHot(rt){
|
||||
const hot=(rt.actors||[]).slice().sort((a,b)=>(b.mailbox_growth||0)-(a.mailbox_growth||0)).slice(0,8);
|
||||
const gmax=Math.max(1,...hot.map(a=>Math.max(0,a.mailbox_growth||0)));
|
||||
elHot.innerHTML=hot.map(a=>{
|
||||
const g=a.mailbox_growth||0;const gc=growthColor(g);
|
||||
return '<div class="bar-row" data-addr="'+esc(a.address)+'">'+
|
||||
'<span class="name">'+shortAddr(a.address)+(a.name?' '+esc(a.name):'')+'</span>'+
|
||||
'<div class="bar-track"><div class="bar-fill" style="width:'+(Math.max(0,g)/gmax*100).toFixed(1)+'%;background:'+gc+'"></div></div>'+
|
||||
'<span class="num">'+fmt(g,1)+'/s</span></div>';
|
||||
}).join('')||'<div class="empty">no actors</div>';
|
||||
}
|
||||
function renderAlerts(rt){
|
||||
const alerted=(rt.actors||[]).filter(a=>a.poisoned||(a.mailbox_growth||0)>1.5);
|
||||
if(!alerted.length){elAlerts.innerHTML='<div class="empty">fleet healthy</div>';return;}
|
||||
alerted.sort((a,b)=>{const ap=a.poisoned?0:1,bp=b.poisoned?0:1;if(ap!==bp)return ap-bp;return (b.mailbox_growth||0)-(a.mailbox_growth||0);});
|
||||
elAlerts.innerHTML='<table class="table"><tbody>'+
|
||||
alerted.map(a=>{
|
||||
const reason=a.poisoned?'poisoned':'saturating';
|
||||
return '<tr data-addr="'+esc(a.address)+'"><td>'+shortAddr(a.address)+(a.name?' '+esc(a.name):'')+' '+stateBadge(a)+'</td>'+
|
||||
'<td class="muted">'+reason+'</td><td style="text-align:right;color:var(--muted);width:1%;">\u2192</td></tr>';
|
||||
}).join('')+'</tbody></table>';
|
||||
}
|
||||
function colValue(a,key){
|
||||
switch(key){
|
||||
case 'actor':return (a.name||a.address).toLowerCase();
|
||||
case 'type':return typeShort(a.actor_type).toLowerCase();
|
||||
case 'state':return a.poisoned?'poisoned':'running';
|
||||
case 'mailbox':return a.mailbox_depth||0;
|
||||
case 'rate':return a.msg_per_sec||0;
|
||||
case 'processed':return a.messages_processed||0;
|
||||
case 'last':return a.last_msg_type||'';
|
||||
case 'worker':return a.worker_id==null?-1:a.worker_id;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function visible(rt){
|
||||
let list=rt.actors||[];
|
||||
if(fSearch){const q=fSearch.toLowerCase();list=list.filter(a=>((a.name?a.name+' ':'')+(a.actor_type?typeShort(a.actor_type)+' '+a.actor_type+' ':'')+a.address+' '+(a.worker_id==null?'':a.worker_id)).toLowerCase().indexOf(q)>=0);}
|
||||
if(fState!=='all')list=list.filter(a=>(a.poisoned?'poisoned':'running')===fState);
|
||||
if(fWorker!=='all')list=list.filter(a=>String(a.worker_id)===fWorker);
|
||||
const k=sortKey,dir=sortDir==='asc'?1:-1;
|
||||
return list.slice().sort((x,y)=>{const vx=colValue(x,k),vy=colValue(y,k);let r;if(typeof vx==='number'&&typeof vy==='number')r=vx-vy;else r=String(vx).localeCompare(String(vy));return r*dir;});
|
||||
}
|
||||
function renderHead(){
|
||||
elHead.innerHTML=COLS.map(c=>{
|
||||
const key=c[0],label=c[1];
|
||||
const arrow=sortKey===key?(sortDir==='asc'?' \u25B2':' \u25BC'):'';
|
||||
const order=sortKey===key?(sortDir==='asc'?'ascending':'descending'):'none';
|
||||
return '<th data-key="'+key+'" scope="col" aria-sort="'+order+'">'+label+arrow+'</th>';
|
||||
}).join('');
|
||||
}
|
||||
function populateWorkerFilter(rt){
|
||||
const seen={};(rt.actors||[]).forEach(a=>{if(a.worker_id!=null)seen[a.worker_id]=1;});
|
||||
const keys=Object.keys(seen).sort((a,b)=>a-b);
|
||||
elWorker.innerHTML='<option value="all">all workers</option>'+keys.map(w=>'<option value="'+esc(w)+'">'+esc(w)+'</option>').join('');
|
||||
if(fWorker!=='all'&&!keys.includes(fWorker))fWorker='all';
|
||||
elWorker.value=fWorker;
|
||||
}
|
||||
function renderRows(rt){
|
||||
const list=visible(rt);
|
||||
if(!list.length){elRows.innerHTML='<tr><td colspan="'+COLS.length+'"><div class="empty">No actors match the current filters.</div></td></tr>';return;}
|
||||
elRows.innerHTML=list.map(a=>{
|
||||
const hist=(a.history||[]).map(h=>h.mailbox_depth);
|
||||
const gc=growthColor(a.mailbox_growth||0);
|
||||
const mbox='<span style="display:inline-flex;align-items:center;gap:8px">'+
|
||||
'<span style="color:'+gc+';font-weight:600">'+fmt(a.mailbox_depth)+'</span>'+sparkline(hist,60,16,gc)+'</span>';
|
||||
const actorCell=shortAddr(a.address)+(a.name?'<br><span class="muted" style="font-size:11px">'+esc(a.name)+'</span>':'');
|
||||
const typeCell=a.actor_type?'<span class="muted" title="'+esc(a.actor_type)+'">'+esc(typeShort(a.actor_type))+'</span>':'<span class="muted">\u2014</span>';
|
||||
return '<tr data-addr="'+esc(a.address)+'">'+
|
||||
'<td>'+actorCell+'</td><td>'+typeCell+'</td><td>'+stateBadge(a)+'</td><td>'+mbox+'</td>'+
|
||||
'<td>'+fmt(a.msg_per_sec,1)+'</td><td>'+fmt(a.messages_processed)+'</td>'+
|
||||
'<td><span class="muted" style="display:inline-block;max-width:240px;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom">'+esc(a.last_msg_type)+'</span></td>'+
|
||||
'<td>'+(a.worker_id==null?'<span class="muted">\u2014</span>':a.worker_id)+'</td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
function render(rt){
|
||||
if(!rt){[elCards,elDist,elThroughput,elWorkers,elHot,elAlerts,elRows].forEach(e=>e.innerHTML='');return;}
|
||||
elEmpty.hidden=true;
|
||||
renderCards(rt);renderDist(rt);renderThroughput(rt);renderWorkers(rt);renderHot(rt);renderAlerts(rt);
|
||||
populateWorkerFilter(rt);renderHead();renderRows(rt);
|
||||
}
|
||||
async function refresh(){
|
||||
try{
|
||||
const res=await fetch('/api/view/swactor/actor-overview',{cache:'no-store'});
|
||||
if(!res.ok)throw new Error('HTTP '+res.status);
|
||||
const data=await res.json();
|
||||
runtimes=data.runtimes||[];
|
||||
renderRuntimeSelector();
|
||||
const rt=pickRuntime(runtimes);
|
||||
if(rt){elEmpty.hidden=true;}else{elEmpty.className='toast';elEmpty.textContent='No runtime actor frames yet. A live swactor runtime publishing runtime.actors will populate this page.';elEmpty.hidden=false;}
|
||||
render(rt);
|
||||
}catch(e){
|
||||
elEmpty.className='toast err';
|
||||
elEmpty.textContent='Failed to load actor frames: '+e.message;
|
||||
elEmpty.hidden=false;
|
||||
}
|
||||
}
|
||||
function setSort(key){if(sortKey===key)sortDir=sortDir==='asc'?'desc':'asc';else{sortKey=key;sortDir=DEFDIR[key]||'asc';}renderHead();renderRows(pickRuntime(runtimes));}
|
||||
elRuntime.addEventListener('change',e=>{runtimeKey=e.target.value;render(pickRuntime(runtimes));});
|
||||
elSearch.addEventListener('input',e=>{fSearch=e.target.value.toLowerCase();renderRows(pickRuntime(runtimes));});
|
||||
elState.addEventListener('change',e=>{fState=e.target.value;renderRows(pickRuntime(runtimes));});
|
||||
elWorker.addEventListener('change',e=>{fWorker=e.target.value;renderRows(pickRuntime(runtimes));});
|
||||
elHead.addEventListener('click',e=>{const th=e.target.closest('th[data-key]');if(th)setSort(th.getAttribute('data-key'));});
|
||||
document.addEventListener('click',e=>{const el=e.target.closest('[data-addr]');if(el)location.href='/view/swactor/actor-dossier?actor='+encodeURIComponent(el.getAttribute('data-addr'));});
|
||||
renderHead();
|
||||
refresh();
|
||||
setInterval(refresh,POLL_MS);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,635 +0,0 @@
|
|||
//! Actor-centric read-only view over `runtime.actors` / `runtime.stats` frames.
|
||||
//!
|
||||
//! Pure frame consumer: it folds incoming per-actor snapshots into local state
|
||||
//! and exposes JSON + HTML. It sends nothing back to observed runtimes. Parsing
|
||||
//! is intentionally tolerant of publisher shape — a frame is a flat record that
|
||||
//! may come from the per-worker `DatastreamStatsHook` envelope
|
||||
//! (`{worker_id, actors:[...]}`) or from a process that publishes a merged
|
||||
//! `{actors:[...]}` payload (the dashboard dummy node, app runtimes). Fields the
|
||||
//! publisher includes (name, worker_id, lifecycle flags) are displayed; ones it
|
||||
//! omits are left blank rather than fabricated.
|
||||
//!
|
||||
//! Reliably on the frame today: address, mailbox depth, throughput, last
|
||||
//! message, poisoned flag, and the message-type diet. `worker_id` and `name`
|
||||
//! appear when the publisher sends them. Finer lifecycle granularity
|
||||
//! (new/suspended/stopping), actor Rust type, and spawn age are not on the
|
||||
//! current frame and are therefore not shown — enriching the feed is a separate
|
||||
//! core concern, not a dashboard one.
|
||||
//!
|
||||
//! Two pages share one data model: the fused overview+roster
|
||||
//! (`/view/swactor/actor-overview`) and the per-actor dossier
|
||||
//! (`/view/swactor/actor-dossier`). Each is a self-contained `DashboardView`
|
||||
//! instance ingesting the same channels.
|
||||
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use datastream::frame::{Frame, StreamId};
|
||||
use parking_lot::RwLock;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::swactor::{RUNTIME_ACTORS, RUNTIME_STATS};
|
||||
use crate::view::DashboardView;
|
||||
use crate::{FrameEvent, StreamEvent};
|
||||
|
||||
const CHANNELS: &[&str] = &[RUNTIME_ACTORS, RUNTIME_STATS];
|
||||
const HISTORY_CAP: usize = 512;
|
||||
const HISTORY_MIN_INTERVAL: Duration = Duration::from_millis(250);
|
||||
const PER_ACTOR_HISTORY_CAP: usize = 120;
|
||||
const GROWTH_WINDOW: usize = 12;
|
||||
const LIVE_TTL: Duration = Duration::from_secs(8);
|
||||
|
||||
/// Read-only actor panel. One instance per served page so each page owns its
|
||||
/// state independently; both ingest the same frames.
|
||||
pub struct ActorPanelView {
|
||||
id: &'static str,
|
||||
title: &'static str,
|
||||
path: &'static str,
|
||||
html: &'static str,
|
||||
state: RwLock<PanelState>,
|
||||
}
|
||||
|
||||
impl ActorPanelView {
|
||||
/// Fused overview + roster at `/view/swactor/actor-overview`.
|
||||
pub fn overview() -> Self {
|
||||
Self::new(
|
||||
"swactor-actor-overview",
|
||||
"Actor overview",
|
||||
"swactor/actor-overview",
|
||||
include_str!("actor_overview.html"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Per-actor dossier at `/view/swactor/actor-dossier`.
|
||||
pub fn dossier() -> Self {
|
||||
Self::new(
|
||||
"swactor-actor-dossier",
|
||||
"Actor dossier",
|
||||
"swactor/actor-dossier",
|
||||
include_str!("actor_dossier.html"),
|
||||
)
|
||||
}
|
||||
|
||||
fn new(id: &'static str, title: &'static str, path: &'static str, html: &'static str) -> Self {
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
path,
|
||||
html,
|
||||
state: RwLock::new(PanelState::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PanelState {
|
||||
runtimes: BTreeMap<String, RuntimeState>,
|
||||
}
|
||||
|
||||
struct RuntimeState {
|
||||
stream: StreamEvent,
|
||||
last_seen: Instant,
|
||||
num_workers: Option<u32>,
|
||||
uptime_ms: Option<u64>,
|
||||
actors: BTreeMap<String, ActorState>,
|
||||
history: VecDeque<HistorySample>,
|
||||
}
|
||||
|
||||
impl RuntimeState {
|
||||
fn new(stream: StreamEvent, now: Instant) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
last_seen: now,
|
||||
num_workers: None,
|
||||
uptime_ms: None,
|
||||
actors: BTreeMap::new(),
|
||||
history: VecDeque::with_capacity(HISTORY_CAP),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, channel: &str, payload: &[u8], now: Instant) {
|
||||
self.last_seen = now;
|
||||
let Ok(value) = serde_json::from_slice::<Value>(payload) else {
|
||||
return;
|
||||
};
|
||||
match channel {
|
||||
RUNTIME_ACTORS => self.apply_actors(&value, now),
|
||||
RUNTIME_STATS => self.apply_stats(&value, now),
|
||||
_ => {}
|
||||
}
|
||||
self.push_history(now);
|
||||
}
|
||||
|
||||
fn apply_actors(&mut self, value: &Value, now: Instant) {
|
||||
// Per-worker `worker_id` wrapper (DatastreamStatsHook shape) is the
|
||||
// default placement for actors that do not carry one inline.
|
||||
let wrapper_worker = u32_field(value, &["worker_id", "worker"]);
|
||||
if let Some(actors) = value.get("actors").and_then(Value::as_array) {
|
||||
for actor in actors {
|
||||
self.apply_actor(actor, now, wrapper_worker);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// A bare actor object per frame (no envelope).
|
||||
self.apply_actor(value, now, wrapper_worker);
|
||||
}
|
||||
|
||||
fn apply_actor(&mut self, value: &Value, now: Instant, default_worker: Option<u32>) {
|
||||
let Some(address) = string_field(value, &["address", "addr", "actor_addr"]) else {
|
||||
return;
|
||||
};
|
||||
let actor = self
|
||||
.actors
|
||||
.entry(address.clone())
|
||||
.or_insert_with(|| ActorState::new(address));
|
||||
actor.apply_json(value, now, default_worker);
|
||||
}
|
||||
|
||||
fn apply_stats(&mut self, value: &Value, now: Instant) {
|
||||
if let Some(num_workers) = u32_field(value, &["num_workers", "workers_live"]) {
|
||||
self.num_workers = Some(num_workers);
|
||||
}
|
||||
if let Some(uptime_ms) = u64_field(value, &["uptime_ms"]) {
|
||||
self.uptime_ms = Some(uptime_ms);
|
||||
}
|
||||
// address -> worker_id mapping; runtime.stats `actors` is [[addr, wid]].
|
||||
if let Some(actors) = value.get("actors").and_then(Value::as_array) {
|
||||
for entry in actors {
|
||||
if let Some(items) = entry.as_array()
|
||||
&& items.len() >= 2
|
||||
&& let (Some(address), Some(worker_id)) =
|
||||
(value_to_string(&items[0]), value_to_u32(&items[1]))
|
||||
{
|
||||
self.actors
|
||||
.entry(address.clone())
|
||||
.or_insert_with(|| ActorState::new(address))
|
||||
.worker_id = Some(worker_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Some publishers carry full per-actor detail under `actor_details`.
|
||||
if let Some(details) = value.get("actor_details").and_then(Value::as_array) {
|
||||
for actor in details {
|
||||
self.apply_actor(actor, now, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_history(&mut self, now: Instant) {
|
||||
let totals = self.totals();
|
||||
if let Some(last) = self.history.back_mut()
|
||||
&& now.duration_since(last.at) < HISTORY_MIN_INTERVAL
|
||||
{
|
||||
last.mailbox_depth = totals.mailbox_depth;
|
||||
last.msg_per_sec = totals.msg_per_sec;
|
||||
return;
|
||||
}
|
||||
if self.history.len() == HISTORY_CAP {
|
||||
self.history.pop_front();
|
||||
}
|
||||
self.history.push_back(HistorySample {
|
||||
at: now,
|
||||
mailbox_depth: totals.mailbox_depth,
|
||||
msg_per_sec: totals.msg_per_sec,
|
||||
});
|
||||
}
|
||||
|
||||
fn totals(&self) -> Totals {
|
||||
let mut totals = Totals::default();
|
||||
totals.actors = self.actors.len().min(u32::MAX as usize) as u32;
|
||||
for actor in self.actors.values() {
|
||||
totals.mailbox_depth = totals.mailbox_depth.saturating_add(actor.mailbox_depth);
|
||||
totals.msg_per_sec += actor.msg_per_sec;
|
||||
if actor.poisoned {
|
||||
totals.poisoned = totals.poisoned.saturating_add(1);
|
||||
}
|
||||
}
|
||||
totals
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ActorState {
|
||||
address: String,
|
||||
name: Option<String>,
|
||||
actor_type: Option<String>,
|
||||
message_type: Option<String>,
|
||||
worker_id: Option<u32>,
|
||||
mailbox_depth: u32,
|
||||
mailbox_growth: f64,
|
||||
messages_processed: u64,
|
||||
msg_per_sec: f64,
|
||||
last_msg_type: Option<String>,
|
||||
poisoned: bool,
|
||||
message_type_counts: Vec<(String, u64)>,
|
||||
history: VecDeque<ActorHistorySample>,
|
||||
last_update: Option<Instant>,
|
||||
}
|
||||
|
||||
impl ActorState {
|
||||
fn new(address: String) -> Self {
|
||||
Self {
|
||||
address,
|
||||
name: None,
|
||||
actor_type: None,
|
||||
message_type: None,
|
||||
worker_id: None,
|
||||
mailbox_depth: 0,
|
||||
mailbox_growth: 0.0,
|
||||
messages_processed: 0,
|
||||
msg_per_sec: 0.0,
|
||||
last_msg_type: None,
|
||||
poisoned: false,
|
||||
message_type_counts: Vec::new(),
|
||||
history: VecDeque::new(),
|
||||
last_update: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_json(&mut self, value: &Value, now: Instant, default_worker: Option<u32>) {
|
||||
let elapsed = self
|
||||
.last_update
|
||||
.map(|then| now.duration_since(then).as_secs_f64())
|
||||
.unwrap_or(0.0);
|
||||
if let Some(name) = string_field(value, &["name"]).filter(|name| !name.is_empty()) {
|
||||
self.name = Some(name);
|
||||
}
|
||||
if let Some(actor_type) = string_field(value, &["actor_type"]).filter(|t| !t.is_empty()) {
|
||||
self.actor_type = Some(actor_type);
|
||||
}
|
||||
if let Some(message_type) = string_field(value, &["message_type"]).filter(|t| !t.is_empty()) {
|
||||
self.message_type = Some(message_type);
|
||||
}
|
||||
if let Some(worker_id) = u32_field(value, &["worker_id", "worker"]) {
|
||||
self.worker_id = Some(worker_id);
|
||||
} else if self.worker_id.is_none() {
|
||||
self.worker_id = default_worker;
|
||||
}
|
||||
assign_u32(&mut self.mailbox_depth, value, &["mailbox_depth", "queued"]);
|
||||
assign_u64_rate(
|
||||
&mut self.messages_processed,
|
||||
&mut self.msg_per_sec,
|
||||
value,
|
||||
&["messages_processed", "processed", "messages_handled"],
|
||||
elapsed,
|
||||
);
|
||||
if let Some(last) = string_field(
|
||||
value,
|
||||
&["last_msg_type", "last_message", "last_message_type"],
|
||||
)
|
||||
.filter(|last| !last.is_empty())
|
||||
{
|
||||
self.last_msg_type = Some(last);
|
||||
}
|
||||
if let Some(poisoned) = value.get("poisoned").and_then(Value::as_bool) {
|
||||
self.poisoned = poisoned;
|
||||
}
|
||||
if let Some(counts) = parse_message_type_counts(value.get("message_type_counts")) {
|
||||
self.message_type_counts = counts;
|
||||
}
|
||||
self.last_update = Some(now);
|
||||
self.push_history(now);
|
||||
self.recompute_growth();
|
||||
}
|
||||
|
||||
fn push_history(&mut self, now: Instant) {
|
||||
if let Some(last) = self.history.back_mut()
|
||||
&& now.duration_since(last.at) < HISTORY_MIN_INTERVAL
|
||||
{
|
||||
last.mailbox_depth = self.mailbox_depth;
|
||||
last.msg_per_sec = self.msg_per_sec;
|
||||
return;
|
||||
}
|
||||
if self.history.len() == PER_ACTOR_HISTORY_CAP {
|
||||
self.history.pop_front();
|
||||
}
|
||||
self.history.push_back(ActorHistorySample {
|
||||
at: now,
|
||||
mailbox_depth: self.mailbox_depth,
|
||||
msg_per_sec: self.msg_per_sec,
|
||||
});
|
||||
}
|
||||
|
||||
/// Mailbox depth/s over the recent window, for trend colouring.
|
||||
fn recompute_growth(&mut self) {
|
||||
let n = self.history.len();
|
||||
if n < 2 {
|
||||
self.mailbox_growth = 0.0;
|
||||
return;
|
||||
}
|
||||
let start = n - n.min(GROWTH_WINDOW);
|
||||
let first = &self.history[start];
|
||||
let last = &self.history[n - 1];
|
||||
let span = last.at.duration_since(first.at).as_secs_f64();
|
||||
self.mailbox_growth = if span > 0.0 {
|
||||
(last.mailbox_depth as f64 - first.mailbox_depth as f64) / span
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct HistorySample {
|
||||
at: Instant,
|
||||
mailbox_depth: u32,
|
||||
msg_per_sec: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ActorHistorySample {
|
||||
at: Instant,
|
||||
mailbox_depth: u32,
|
||||
msg_per_sec: f64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Totals {
|
||||
actors: u32,
|
||||
mailbox_depth: u32,
|
||||
msg_per_sec: f64,
|
||||
poisoned: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PanelSnapshot {
|
||||
runtimes: Vec<RuntimeSnapshot>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RuntimeSnapshot {
|
||||
stream: StreamSnapshot,
|
||||
live: bool,
|
||||
last_seen_ms_ago: u64,
|
||||
summary: SummarySnapshot,
|
||||
actors: Vec<ActorSnapshot>,
|
||||
history: Vec<HistorySnapshot>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StreamSnapshot {
|
||||
key: String,
|
||||
node: String,
|
||||
life: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SummarySnapshot {
|
||||
actors: u32,
|
||||
msg_per_sec: f64,
|
||||
mailbox_depth: u32,
|
||||
poisoned: u32,
|
||||
uptime_ms: Option<u64>,
|
||||
num_workers: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ActorSnapshot {
|
||||
address: String,
|
||||
name: Option<String>,
|
||||
actor_type: Option<String>,
|
||||
message_type: Option<String>,
|
||||
worker_id: Option<u32>,
|
||||
/// Single derived display state. The frame carries only `poisoned`, so the
|
||||
/// granularity is poisoned | running until the feed is enriched.
|
||||
state: &'static str,
|
||||
mailbox_depth: u32,
|
||||
mailbox_growth: f64,
|
||||
messages_processed: u64,
|
||||
msg_per_sec: f64,
|
||||
last_msg_type: Option<String>,
|
||||
poisoned: bool,
|
||||
message_type_counts: Vec<MessageTypeCountSnapshot>,
|
||||
history: Vec<ActorHistorySnapshot>,
|
||||
last_seen_ms_ago: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MessageTypeCountSnapshot {
|
||||
message_type: String,
|
||||
count: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct HistorySnapshot {
|
||||
ms_ago: u64,
|
||||
msg_per_sec: f64,
|
||||
mailbox_depth: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ActorHistorySnapshot {
|
||||
ms_ago: u64,
|
||||
mailbox_depth: u32,
|
||||
msg_per_sec: f64,
|
||||
}
|
||||
|
||||
impl DashboardView for ActorPanelView {
|
||||
fn id(&self) -> &'static str {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn title(&self) -> &'static str {
|
||||
self.title
|
||||
}
|
||||
|
||||
fn path(&self) -> &'static str {
|
||||
self.path
|
||||
}
|
||||
|
||||
fn channels(&self) -> &'static [&'static str] {
|
||||
CHANNELS
|
||||
}
|
||||
|
||||
fn ingest(&self, _stream: &StreamId, _frame: &Frame, event: &FrameEvent) {
|
||||
let now = Instant::now();
|
||||
let mut state = self.state.write();
|
||||
let key = stream_key(&event.stream);
|
||||
state
|
||||
.runtimes
|
||||
.entry(key)
|
||||
.or_insert_with(|| RuntimeState::new(event.stream.clone(), now))
|
||||
.update(&event.channel, &event.payload, now);
|
||||
}
|
||||
|
||||
fn snapshot_json(&self) -> Value {
|
||||
let now = Instant::now();
|
||||
let snapshot = PanelSnapshot {
|
||||
runtimes: self
|
||||
.state
|
||||
.read()
|
||||
.runtimes
|
||||
.values()
|
||||
.map(|runtime| runtime_snapshot(runtime, now))
|
||||
.collect(),
|
||||
};
|
||||
serde_json::to_value(snapshot).unwrap_or_else(|_| json!({ "runtimes": [] }))
|
||||
}
|
||||
|
||||
fn html(&self) -> Option<&'static str> {
|
||||
Some(self.html)
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_snapshot(runtime: &RuntimeState, now: Instant) -> RuntimeSnapshot {
|
||||
let totals = runtime.totals();
|
||||
RuntimeSnapshot {
|
||||
stream: StreamSnapshot {
|
||||
key: stream_key(&runtime.stream),
|
||||
node: runtime.stream.node.clone(),
|
||||
life: runtime.stream.life,
|
||||
},
|
||||
live: now.duration_since(runtime.last_seen) <= LIVE_TTL,
|
||||
last_seen_ms_ago: now.duration_since(runtime.last_seen).as_millis() as u64,
|
||||
summary: SummarySnapshot {
|
||||
actors: totals.actors,
|
||||
msg_per_sec: totals.msg_per_sec,
|
||||
mailbox_depth: totals.mailbox_depth,
|
||||
poisoned: totals.poisoned,
|
||||
uptime_ms: runtime.uptime_ms,
|
||||
num_workers: runtime.num_workers,
|
||||
},
|
||||
actors: runtime
|
||||
.actors
|
||||
.values()
|
||||
.map(|actor| ActorSnapshot {
|
||||
address: actor.address.clone(),
|
||||
name: actor.name.clone(),
|
||||
actor_type: actor.actor_type.clone(),
|
||||
message_type: actor.message_type.clone(),
|
||||
worker_id: actor.worker_id,
|
||||
state: if actor.poisoned { "poisoned" } else { "running" },
|
||||
mailbox_depth: actor.mailbox_depth,
|
||||
mailbox_growth: actor.mailbox_growth,
|
||||
messages_processed: actor.messages_processed,
|
||||
msg_per_sec: actor.msg_per_sec,
|
||||
last_msg_type: actor.last_msg_type.clone(),
|
||||
poisoned: actor.poisoned,
|
||||
message_type_counts: actor
|
||||
.message_type_counts
|
||||
.iter()
|
||||
.map(|(message_type, count)| MessageTypeCountSnapshot {
|
||||
message_type: message_type.clone(),
|
||||
count: *count,
|
||||
})
|
||||
.collect(),
|
||||
history: actor
|
||||
.history
|
||||
.iter()
|
||||
.map(|sample| ActorHistorySnapshot {
|
||||
ms_ago: now.duration_since(sample.at).as_millis() as u64,
|
||||
mailbox_depth: sample.mailbox_depth,
|
||||
msg_per_sec: sample.msg_per_sec,
|
||||
})
|
||||
.collect(),
|
||||
last_seen_ms_ago: actor
|
||||
.last_update
|
||||
.map(|then| now.duration_since(then).as_millis() as u64)
|
||||
.unwrap_or(0),
|
||||
})
|
||||
.collect(),
|
||||
history: runtime
|
||||
.history
|
||||
.iter()
|
||||
.map(|sample| HistorySnapshot {
|
||||
ms_ago: now.duration_since(sample.at).as_millis() as u64,
|
||||
msg_per_sec: sample.msg_per_sec,
|
||||
mailbox_depth: sample.mailbox_depth,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// --- tolerant JSON helpers (publisher-shape-agnostic readers) ---------------
|
||||
|
||||
fn assign_u32(slot: &mut u32, value: &Value, names: &[&str]) {
|
||||
if let Some(v) = u32_field(value, names) {
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
|
||||
fn assign_u64_rate(slot: &mut u64, rate: &mut f64, value: &Value, names: &[&str], elapsed: f64) {
|
||||
if let Some(next) = u64_field(value, names) {
|
||||
if elapsed > 0.0 && next > *slot {
|
||||
*rate = (next - *slot) as f64 / elapsed;
|
||||
} else if next < *slot {
|
||||
*rate = 0.0;
|
||||
}
|
||||
*slot = next;
|
||||
}
|
||||
}
|
||||
|
||||
fn u32_field(value: &Value, names: &[&str]) -> Option<u32> {
|
||||
u64_field(value, names).and_then(|v| u32::try_from(v).ok())
|
||||
}
|
||||
|
||||
fn u64_field(value: &Value, names: &[&str]) -> Option<u64> {
|
||||
names
|
||||
.iter()
|
||||
.find_map(|name| value.get(*name).and_then(value_to_u64))
|
||||
}
|
||||
|
||||
fn string_field(value: &Value, names: &[&str]) -> Option<String> {
|
||||
names
|
||||
.iter()
|
||||
.find_map(|name| value.get(*name).and_then(value_to_string))
|
||||
}
|
||||
|
||||
fn value_to_u32(value: &Value) -> Option<u32> {
|
||||
value_to_u64(value).and_then(|v| u32::try_from(v).ok())
|
||||
}
|
||||
|
||||
fn value_to_u64(value: &Value) -> Option<u64> {
|
||||
value
|
||||
.as_u64()
|
||||
.or_else(|| value.as_str().and_then(|s| s.parse::<u64>().ok()))
|
||||
}
|
||||
|
||||
fn value_to_string(value: &Value) -> Option<String> {
|
||||
value.as_str().map(ToOwned::to_owned).or_else(|| {
|
||||
if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Accepts pair form `["Type", N]`, object form `{"ty": "Type", "count": N}`,
|
||||
/// and map form `{"Type": N}`.
|
||||
fn parse_message_type_counts(value: Option<&Value>) -> Option<Vec<(String, u64)>> {
|
||||
let value = value?;
|
||||
if let Some(items) = value.as_array() {
|
||||
let mut out = Vec::new();
|
||||
for item in items {
|
||||
if let Some(pair) = item.as_array()
|
||||
&& pair.len() >= 2
|
||||
&& let (Some(name), Some(count)) =
|
||||
(value_to_string(&pair[0]), value_to_u64(&pair[1]))
|
||||
{
|
||||
out.push((name, count));
|
||||
continue;
|
||||
}
|
||||
if let Some(name) = string_field(item, &["ty", "message_type", "type", "name"])
|
||||
&& let Some(count) = u64_field(item, &["count"])
|
||||
{
|
||||
out.push((name, count));
|
||||
}
|
||||
}
|
||||
return Some(out);
|
||||
}
|
||||
if let Some(map) = value.as_object() {
|
||||
let mut out: Vec<(String, u64)> = map
|
||||
.iter()
|
||||
.filter_map(|(name, count)| value_to_u64(count).map(|count| (name.clone(), count)))
|
||||
.collect();
|
||||
out.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
return Some(out);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn stream_key(stream: &StreamEvent) -> String {
|
||||
format!("{}#{}", stream.node, stream.life)
|
||||
}
|
||||
|
|
@ -2,12 +2,10 @@ use std::sync::Arc;
|
|||
|
||||
use crate::view::DashboardView;
|
||||
|
||||
mod actor_view;
|
||||
mod worker_page;
|
||||
mod worker_view;
|
||||
|
||||
pub use worker_view::SwactorWorkerView;
|
||||
pub use actor_view::ActorPanelView;
|
||||
|
||||
pub const RUNTIME_STATS: &str = "runtime.stats";
|
||||
pub const RUNTIME_WORKERS: &str = "runtime.workers";
|
||||
|
|
@ -16,13 +14,3 @@ pub const RUNTIME_ACTORS: &str = "runtime.actors";
|
|||
pub fn worker_view() -> Arc<dyn DashboardView> {
|
||||
Arc::new(SwactorWorkerView::default())
|
||||
}
|
||||
|
||||
/// Built-in actor overview + roster view (`/view/swactor/actor-overview`).
|
||||
pub fn actor_overview_view() -> Arc<dyn DashboardView> {
|
||||
Arc::new(ActorPanelView::overview())
|
||||
}
|
||||
|
||||
/// Built-in actor dossier view (`/view/swactor/actor-dossier`).
|
||||
pub fn actor_dossier_view() -> Arc<dyn DashboardView> {
|
||||
Arc::new(ActorPanelView::dossier())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
name = "data-plane"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
523
crates/data-plane/DATA_PLANE_ACTOR_SPEC.md
Normal file
523
crates/data-plane/DATA_PLANE_ACTOR_SPEC.md
Normal file
|
|
@ -0,0 +1,523 @@
|
|||
# Data Plane Actor Architecture Specification
|
||||
|
||||
**Status:** implemented architecture contract for the `data-plane` crate.
|
||||
|
||||
This document describes the behavioral boundary between reusable data-plane actors
|
||||
and MVP-specific orchestration/runtime code. It is intentionally architectural: it
|
||||
names responsibilities, actor roles, message families, and ownership boundaries
|
||||
without prescribing file layout or migration steps.
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
`data-plane` owns the behavior required to move model objects between stages and
|
||||
between a node process and its local GPU worker process.
|
||||
|
||||
The crate defines the actor protocol for:
|
||||
|
||||
- provisioning logical data edges;
|
||||
- distinguishing network/wire edges from node-local IPC rings;
|
||||
- leasing, installing, readying, faulting, stopping, quiescing, and releasing
|
||||
local rings;
|
||||
- binding logical edges to transport endpoints and local worker rings;
|
||||
- parsing, validating, sequencing, loading, producing, and reporting objects;
|
||||
- gating readiness and object visibility on the correct lifecycle transitions;
|
||||
- translating low-level arena, transport, and worker observations into coarse
|
||||
data-plane outcomes.
|
||||
|
||||
`mvp-system` uses `data-plane` as a reusable actor subsystem. It provides run
|
||||
intent, concrete runtime actor addresses, and MVP-specific report sinks. It does
|
||||
not own the fine-grained data-plane state machine.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core boundary
|
||||
|
||||
Swactor actors carry control, lifecycle, and identity messages. Payload bytes do
|
||||
not move through actor mailboxes.
|
||||
|
||||
Payload bytes move through:
|
||||
|
||||
- arena-backed shared-memory rings for node-local IPC;
|
||||
- transport byte streams for node-to-node data edges;
|
||||
- GPU-worker-owned device allocations for compute-ready objects.
|
||||
|
||||
The data-plane actors decide when these byte paths are established, readable,
|
||||
writable, faulted, stopped, and safe to release. Runtime adapter actors execute
|
||||
concrete effects and report observations back.
|
||||
|
||||
---
|
||||
|
||||
## 3. Edge and ring model
|
||||
|
||||
### 3.1 Wire edge
|
||||
|
||||
A wire edge is a logical run-plan connection between a producer endpoint and a
|
||||
consumer endpoint.
|
||||
|
||||
It carries:
|
||||
|
||||
- run identity;
|
||||
- edge identity;
|
||||
- producer and consumer node identity;
|
||||
- edge kind, such as token input, activation, or token output;
|
||||
- object contract;
|
||||
- transport contract;
|
||||
- optional remote endpoint and remote actor identity.
|
||||
|
||||
A wire edge answers: "which logical data stream connects these stage endpoints?"
|
||||
|
||||
It does not answer: "which local arena offset or worker-process ring is being
|
||||
used on this node?"
|
||||
|
||||
### 3.2 Local IPC ring
|
||||
|
||||
A local IPC ring is a node-local buffer used by a Rust node process and its owned
|
||||
GPU worker process.
|
||||
|
||||
It carries:
|
||||
|
||||
- ring identity allocated by the local arena manager;
|
||||
- arena layout and capacity;
|
||||
- worker-process generation;
|
||||
- local role port, such as input or output;
|
||||
- direction relative to the worker process;
|
||||
- object contract installed into the worker;
|
||||
- quiescence and release state.
|
||||
|
||||
A local IPC ring answers: "how does this node exchange bytes with its local
|
||||
worker process for a specific edge endpoint?"
|
||||
|
||||
It does not answer: "which remote node or distributed route owns the other side
|
||||
of the logical edge?"
|
||||
|
||||
### 3.3 Binding
|
||||
|
||||
A wire edge endpoint may bind to zero or more local resources depending on its
|
||||
role:
|
||||
|
||||
```text
|
||||
inbound wire edge endpoint
|
||||
-> recv transport endpoint
|
||||
-> local worker ingress ring
|
||||
-> device object handle
|
||||
|
||||
outbound wire edge endpoint
|
||||
-> local worker egress ring
|
||||
-> send transport endpoint
|
||||
|
||||
local-only edge endpoint
|
||||
-> local producer/consumer binding
|
||||
-> optional worker ring
|
||||
```
|
||||
|
||||
The binding is owned by data-plane state. MVP code may observe the binding only
|
||||
through coarse reports such as edge ready, object loaded, object produced, edge
|
||||
faulted, and edge stopped.
|
||||
|
||||
---
|
||||
|
||||
## 4. Actor topology
|
||||
|
||||
The target topology is actor-oriented.
|
||||
|
||||
### 4.1 Data-plane node actor
|
||||
|
||||
One data-plane node actor owns the data-plane state for one local node within one
|
||||
active run.
|
||||
|
||||
It owns:
|
||||
|
||||
- local node identity;
|
||||
- run-scoped edge table;
|
||||
- mapping from wire edge endpoints to local rings;
|
||||
- mapping from ring ids to edge endpoints;
|
||||
- object sequence state;
|
||||
- device-handle visibility state;
|
||||
- data-plane child actor addresses;
|
||||
- MVP report sink addresses.
|
||||
|
||||
It receives provisioning intent from MVP code and observations from runtime
|
||||
adapters. It emits actor messages to arena, worker, transport, and MVP report
|
||||
sinks.
|
||||
|
||||
### 4.2 Wire edge actor
|
||||
|
||||
A wire edge actor owns the lifecycle of one logical edge endpoint on the local
|
||||
node.
|
||||
|
||||
It owns:
|
||||
|
||||
- provisioning state;
|
||||
- transport establishment state;
|
||||
- send/receive pump readiness;
|
||||
- stream faults;
|
||||
- logical edge readiness;
|
||||
- stop and fault propagation for that edge endpoint.
|
||||
|
||||
It does not own worker-process state or arena layout details except through a
|
||||
binding supplied by the data-plane node actor or ring actor.
|
||||
|
||||
### 4.3 Local worker ring actor
|
||||
|
||||
A local worker ring actor owns one local IPC ring lifecycle.
|
||||
|
||||
It owns:
|
||||
|
||||
- arena lease request and result;
|
||||
- worker ring installation;
|
||||
- worker ring readable/writable notifications;
|
||||
- ring fault and quiescence observations;
|
||||
- release proof collection;
|
||||
- arena lease release.
|
||||
|
||||
It does not own remote endpoint routing. It can be bound to a wire edge endpoint
|
||||
by edge id, but the ring lifecycle remains local.
|
||||
|
||||
### 4.4 GPU worker control adapter
|
||||
|
||||
The GPU worker control adapter is the actor-facing boundary to the owned Python
|
||||
worker process.
|
||||
|
||||
It owns or fronts:
|
||||
|
||||
- worker process generation;
|
||||
- command serialization to the worker;
|
||||
- stdout/stderr event parsing;
|
||||
- device handle generation checks;
|
||||
- worker stop/crash/restart observations.
|
||||
|
||||
The data plane treats this as an actor endpoint. Worker-process JSON and Python
|
||||
helper details are not exposed to MVP stage logic.
|
||||
|
||||
### 4.5 Transport adapter actors
|
||||
|
||||
Transport adapter actors own concrete wire byte movement.
|
||||
|
||||
They own or front:
|
||||
|
||||
- accepted edge streams;
|
||||
- outbound edge streams;
|
||||
- edge preamble validation;
|
||||
- byte read/write readiness;
|
||||
- transport-specific stream faults;
|
||||
- pump stop observations.
|
||||
|
||||
The data plane treats transport events as observations on a wire edge. Transport
|
||||
actors do not decide stage readiness or object admission.
|
||||
|
||||
### 4.6 MVP report sink
|
||||
|
||||
The MVP report sink receives coarse data-plane outcomes and maps them to
|
||||
MVP-specific control messages.
|
||||
|
||||
Examples:
|
||||
|
||||
- inbound edge ready;
|
||||
- outbound edge ready;
|
||||
- object loaded for stage execution;
|
||||
- object produced for downstream transport;
|
||||
- edge faulted;
|
||||
- local edges stopped.
|
||||
|
||||
The sink does not inspect ring cursors, arena leases, worker generations, or
|
||||
transport pump internals.
|
||||
|
||||
---
|
||||
|
||||
## 5. Actor API surface
|
||||
|
||||
Concrete Rust names are schematic. The contract is the message shape and
|
||||
ownership boundary.
|
||||
|
||||
### 5.1 Provisioning input
|
||||
|
||||
MVP sends provisioning intent to the data-plane node actor:
|
||||
|
||||
```text
|
||||
ProvisionDataPlaneRun {
|
||||
run_id,
|
||||
local_node_id,
|
||||
arena_actor,
|
||||
worker_actor,
|
||||
transport_actor,
|
||||
report_sink,
|
||||
}
|
||||
|
||||
ProvisionWireEdgeEndpoint {
|
||||
run_id,
|
||||
edge_id,
|
||||
direction,
|
||||
edge_kind,
|
||||
local_role_port,
|
||||
local_node_id,
|
||||
peer_node_id,
|
||||
peer_endpoint,
|
||||
object_spec,
|
||||
transport_spec,
|
||||
local_ring_spec,
|
||||
}
|
||||
```
|
||||
|
||||
`direction` is relative to the local node's stage role: inbound means the local
|
||||
stage consumes objects from the edge; outbound means the local stage produces
|
||||
objects to the edge.
|
||||
|
||||
Provisioning is declarative. MVP describes the intended edge endpoint and the
|
||||
actors available to execute effects. It does not prescribe lease/install/driver
|
||||
ordering.
|
||||
|
||||
### 5.2 Runtime observations
|
||||
|
||||
Runtime adapters report observations back to data-plane actors:
|
||||
|
||||
```text
|
||||
ArenaRingLeased
|
||||
ArenaRingLeaseRejected
|
||||
ArenaRingReleased
|
||||
ArenaRingReleaseRejected
|
||||
|
||||
WorkerReady
|
||||
WorkerRingInstalled
|
||||
WorkerRingFaulted
|
||||
WorkerRingQuiesced
|
||||
WorkerRingReadable
|
||||
WorkerRingWritable
|
||||
WorkerObjectLoaded
|
||||
WorkerObjectProduced
|
||||
WorkerObjectFailed
|
||||
WorkerStopped
|
||||
WorkerFaulted
|
||||
|
||||
TransportEdgeReady
|
||||
TransportBytesReceived
|
||||
TransportBytesSent
|
||||
TransportStreamClosed
|
||||
TransportStreamFaulted
|
||||
TransportPumpStopped
|
||||
```
|
||||
|
||||
Observations are facts, not commands. The data plane decides the next state and
|
||||
any follow-up messages.
|
||||
|
||||
### 5.3 Data-plane effects
|
||||
|
||||
The data plane sends effect requests to runtime adapter actors:
|
||||
|
||||
```text
|
||||
LeaseArenaRing
|
||||
CancelArenaRingLease
|
||||
ReleaseArenaRingLease
|
||||
|
||||
InstallWorkerRing
|
||||
UninstallWorkerRing
|
||||
NotifyWorkerRingReadable
|
||||
NotifyWorkerRingWritable
|
||||
LoadObjectFromWorkerRing
|
||||
ExecuteWorkerStep
|
||||
ReleaseWorkerDeviceObject
|
||||
|
||||
EstablishWireSend
|
||||
EstablishWireRecv
|
||||
WriteWireObject
|
||||
StopWirePump
|
||||
```
|
||||
|
||||
Effects are actor messages. The receiving adapter owns the concrete mechanism:
|
||||
memfd/mmap, JSON stdin/stdout, process supervision, iroh streams, or test doubles.
|
||||
|
||||
### 5.4 Data-plane reports
|
||||
|
||||
The data plane reports only stable semantic outcomes to MVP:
|
||||
|
||||
```text
|
||||
InboundEdgeReady { edge_id }
|
||||
OutboundEdgeReady { edge_id }
|
||||
ObjectLoaded { edge_id, object_id, sequence, device_handle }
|
||||
ObjectProduced { edge_id, object_id, sequence, extent }
|
||||
EdgeFaulted { edge_id, reason }
|
||||
EdgeStopped { edge_id }
|
||||
LocalEdgesStopped { run_id }
|
||||
WorkerDataPlaneFaulted { reason }
|
||||
```
|
||||
|
||||
Reports are the only data-plane messages MVP stage/orchestrator actors should
|
||||
need for normal stage progression.
|
||||
|
||||
---
|
||||
|
||||
## 6. Behavior owned by data-plane
|
||||
|
||||
### 6.1 Edge establishment
|
||||
|
||||
For each provisioned edge endpoint, data-plane actors own the establishment
|
||||
sequence.
|
||||
|
||||
Inbound endpoint:
|
||||
|
||||
```text
|
||||
provision endpoint
|
||||
-> lease local ingress ring
|
||||
-> install ring into worker input port
|
||||
-> establish receive transport if the edge is remote
|
||||
-> report inbound edge ready
|
||||
```
|
||||
|
||||
Outbound endpoint:
|
||||
|
||||
```text
|
||||
provision endpoint
|
||||
-> lease local egress ring when worker output is required
|
||||
-> install ring into worker output port
|
||||
-> establish send transport if the edge is remote
|
||||
-> report outbound edge ready
|
||||
```
|
||||
|
||||
Readiness is reported only after every required local and wire resource for that
|
||||
endpoint is ready. A local-only endpoint may omit transport establishment. A
|
||||
wire-only endpoint may omit worker-ring establishment when it terminates outside
|
||||
the local GPU worker.
|
||||
|
||||
### 6.2 Object ingress
|
||||
|
||||
For inbound data, data-plane actors own object admission.
|
||||
|
||||
The data plane:
|
||||
|
||||
- associates incoming bytes with the correct wire edge and stream;
|
||||
- validates object framing and object spec constraints;
|
||||
- preserves sequence ordering required by the edge contract;
|
||||
- writes or exposes the object through the local ingress ring;
|
||||
- asks the worker to load the object to device;
|
||||
- waits for a valid worker object-loaded observation;
|
||||
- reports object loaded to MVP only after the device handle is current and the
|
||||
logical object is complete.
|
||||
|
||||
MVP does not parse object headers, track ingress buffers, reload cursors, or gate
|
||||
object-loaded visibility.
|
||||
|
||||
### 6.3 Object egress
|
||||
|
||||
For outbound data, data-plane actors own object production and forwarding.
|
||||
|
||||
The data plane:
|
||||
|
||||
- receives compute/output observations from the worker;
|
||||
- binds produced objects to the correct outbound edge and sequence;
|
||||
- validates object extent and object contract;
|
||||
- publishes readable/writable state to the worker and transport actors;
|
||||
- forwards complete object records on the wire when the edge is remote;
|
||||
- reports object produced or step-visible outcomes to MVP at semantic
|
||||
boundaries, not cursor boundaries.
|
||||
|
||||
MVP does not decide when a local output ring is readable, when a transport stream
|
||||
should consume it, or when a produced object is safe to expose downstream.
|
||||
|
||||
### 6.4 Faults
|
||||
|
||||
The data plane owns data movement fault classification and propagation.
|
||||
|
||||
Fault sources include:
|
||||
|
||||
- arena lease rejection or release rejection;
|
||||
- worker ring installation failure;
|
||||
- worker ring fault;
|
||||
- malformed object framing;
|
||||
- sequence violation;
|
||||
- worker object load/produce failure;
|
||||
- transport stream read/write/protocol failure;
|
||||
- pump stop before quiescence;
|
||||
- stale worker generation or device handle.
|
||||
|
||||
A fault on one edge endpoint must not silently corrupt another endpoint. The data
|
||||
plane maps local faults to edge-scoped or worker-scoped reports, starts the
|
||||
required stop/quiescence path, and emits the appropriate MVP report.
|
||||
|
||||
### 6.5 Stop, quiescence, and release
|
||||
|
||||
The data plane owns teardown ordering for local resources.
|
||||
|
||||
For a bound edge/ring pair, stop requires:
|
||||
|
||||
```text
|
||||
stop transport pump if present
|
||||
-> uninstall or quiesce worker ring if installed
|
||||
-> prove no local reader/writer still uses the ring
|
||||
-> release arena lease
|
||||
-> report edge stopped
|
||||
```
|
||||
|
||||
Arena release must be gated by quiescence proof. MVP may request run or edge
|
||||
stop, but it does not supply low-level release proof or decide when a ring is
|
||||
safe to release.
|
||||
|
||||
---
|
||||
|
||||
## 7. MVP-system utilization
|
||||
|
||||
`mvp-system` remains responsible for MVP orchestration and stage semantics.
|
||||
|
||||
It owns:
|
||||
|
||||
- run planning and edge assignment;
|
||||
- stage provisioning authority;
|
||||
- membership/readiness gates outside the data plane;
|
||||
- weight loading and role configuration intent;
|
||||
- stage controller behavior;
|
||||
- prompt injection and token consumption;
|
||||
- mapping data-plane reports to MVP lifecycle messages;
|
||||
- selecting concrete runtime adapters for arena, worker process, and transport.
|
||||
|
||||
For data movement, MVP code acts as a client:
|
||||
|
||||
1. Spawn or obtain actor addresses for the data-plane node actor and required
|
||||
runtime adapters.
|
||||
2. Send run and edge provisioning intent to the data-plane node actor.
|
||||
3. Forward runtime observations from concrete adapter actors when those adapters
|
||||
are MVP-owned.
|
||||
4. Receive coarse data-plane reports.
|
||||
5. Translate those reports into stage-controller or orchestrator messages.
|
||||
|
||||
MVP must not rely on private edge states such as waiting-for-lease,
|
||||
waiting-for-worker-ring, waiting-for-driver, pump-stopped, ring-quiesced, or
|
||||
release-ready. Those are data-plane implementation states.
|
||||
|
||||
---
|
||||
|
||||
## 8. Required invariants
|
||||
|
||||
- `EdgeId` names a logical run-plan edge, not a local ring allocation.
|
||||
- `RingId` names a local arena-backed IPC ring, not a distributed edge.
|
||||
- A ring may be bound to an edge endpoint, but the identifiers are not
|
||||
interchangeable.
|
||||
- Actor messages carry control and identities, not tensor payload bytes.
|
||||
- Object-loaded reports are emitted only for complete, validated objects with
|
||||
current-generation device handles.
|
||||
- Edge-ready reports are emitted only after required wire and local IPC resources
|
||||
are ready.
|
||||
- Arena lease release is gated by local quiescence proof.
|
||||
- Worker process generation is part of every device-handle validity decision.
|
||||
- Transport faults and worker faults are classified by data-plane before they
|
||||
become MVP reports.
|
||||
- MVP stage logic observes semantic outcomes, never ring cursor mechanics.
|
||||
|
||||
---
|
||||
|
||||
## 9. Non-goals
|
||||
|
||||
`data-plane` does not own:
|
||||
|
||||
- global run planning;
|
||||
- placement optimization;
|
||||
- model layer assignment;
|
||||
- weight download or weight loading semantics;
|
||||
- prompt tokenization or output token policy;
|
||||
- membership convergence;
|
||||
- provider provisioning;
|
||||
- concrete iroh endpoint construction;
|
||||
- concrete Python helper implementation.
|
||||
|
||||
The crate defines reusable actor protocols and data-movement behavior. Concrete
|
||||
runtime adapters may live beside MVP code, inside reusable support crates, or in
|
||||
tests, as long as they satisfy the actor contracts above.
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
name = "datastream"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[dependencies]
|
||||
swactor = { path = "../..", features = ["serde", "transport"] }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
# The Datastream — Specification
|
||||
|
||||
Id: 7
|
||||
Last modified:
|
||||
Last reviewed:
|
||||
> **Status.** Current-state specification for the `datastream` crate. This keeps
|
||||
> the original outline, but updates the model to the implementation that now
|
||||
> exists: stream-local numeric channel ids, a channel catalog, broadcast endpoint
|
||||
> subscriptions, and catalog-aware transport events.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -758,11 +758,9 @@ impl<'a> RuntimeActorStatsRecord<'a> {
|
|||
actors: snapshots
|
||||
.iter()
|
||||
.map(|snapshot| RuntimeActorSnapshotRecord {
|
||||
address: snapshot.address.to_full_hex(),
|
||||
address: snapshot.address.to_string(),
|
||||
mailbox_depth: snapshot.mailbox_depth,
|
||||
last_msg_type: snapshot.last_msg_type,
|
||||
actor_type: snapshot.actor_type,
|
||||
message_type: snapshot.message_type,
|
||||
messages_processed: snapshot.messages_processed,
|
||||
poisoned: snapshot.poisoned,
|
||||
message_type_counts: snapshot
|
||||
|
|
@ -781,8 +779,6 @@ struct RuntimeActorSnapshotRecord<'a> {
|
|||
address: String,
|
||||
mailbox_depth: usize,
|
||||
last_msg_type: Option<&'static str>,
|
||||
actor_type: Option<&'static str>,
|
||||
message_type: Option<&'static str>,
|
||||
messages_processed: u64,
|
||||
poisoned: bool,
|
||||
message_type_counts: Vec<RuntimeMessageTypeCount<'a>>,
|
||||
|
|
|
|||
|
|
@ -300,8 +300,6 @@ fn stats_hook_adapter_submits_worker_snapshot_json() {
|
|||
address: actor,
|
||||
mailbox_depth: 3,
|
||||
last_msg_type: Some("Ping"),
|
||||
actor_type: Some("TestActor"),
|
||||
message_type: Some("Ping"),
|
||||
messages_processed: 5,
|
||||
poisoned: false,
|
||||
message_type_counts: vec![("Ping", 5)],
|
||||
|
|
@ -315,13 +313,11 @@ fn stats_hook_adapter_submits_worker_snapshot_json() {
|
|||
assert_eq!(delivery.channel.channel, runtime);
|
||||
let json: Value = serde_json::from_slice(&delivery.payload).unwrap();
|
||||
assert_eq!(json["worker_id"], 2);
|
||||
assert_eq!(json["actors"][0]["address"], actor.to_full_hex());
|
||||
assert_eq!(json["actors"][0]["address"], actor.to_string());
|
||||
assert_eq!(json["actors"][0]["mailbox_depth"], 3);
|
||||
assert_eq!(json["actors"][0]["last_msg_type"], "Ping");
|
||||
assert_eq!(json["actors"][0]["messages_processed"], 5);
|
||||
assert_eq!(json["actors"][0]["message_type_counts"][0]["ty"], "Ping");
|
||||
assert_eq!(json["actors"][0]["actor_type"], "TestActor");
|
||||
assert_eq!(json["actors"][0]["message_type"], "Ping");
|
||||
}
|
||||
|
||||
fn positions(events: &[DatastreamEvent]) -> Vec<u64> {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
name = "distribution"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
name = "iroh-driver"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lib]
|
||||
name = "iroh_driver"
|
||||
|
|
@ -20,8 +19,9 @@ iroh = "0.98"
|
|||
# `test-utils` exposes `CaRootsConfig::insecure_skip_verify()` so clients can
|
||||
# trust operator-controlled custom relays with self-signed QAD certs.
|
||||
iroh-relay = { version = "0.98", features = ["test-utils"] }
|
||||
tokio.workspace = true
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "time"] }
|
||||
parking_lot = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
iroh-relay = { version = "0.98", features = ["server", "test-utils"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time"] }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Iroh Driver Fixed Specification
|
||||
|
||||
Id: 5
|
||||
Last modified:
|
||||
Last reviewed:
|
||||
**Status:** draft current-state specification for `iroh-driver`.
|
||||
|
||||
> Review checkpoint: reviewed through Section 3.2; resume with Section 3.3 Accepted Connection Output.
|
||||
|
||||
47
crates/mvp-system/Cargo.toml
Normal file
47
crates/mvp-system/Cargo.toml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
[package]
|
||||
name = "mvp-system"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
autobins = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
dashboard = []
|
||||
|
||||
[dependencies]
|
||||
datastream = { path = "../datastream" }
|
||||
data-plane = { path = "../data-plane" }
|
||||
provisioning = { path = "../provisioning" }
|
||||
dashboard = { path = "../dashboard" }
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
swactor = { path = "../..", features = ["serde", "transport"] }
|
||||
swactor-transport = { path = "../transport" }
|
||||
swactor-process = { path = "../process" }
|
||||
distribution = { path = "../distribution" }
|
||||
iroh-driver = { path = "../iroh-driver" }
|
||||
iroh = "0.98"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "io-util", "sync", "time", "net", "signal"] }
|
||||
swactor-vastai = { path = "../../tools/vastai" }
|
||||
parking_lot = "0.12"
|
||||
blake3 = "1"
|
||||
toml = "0.8"
|
||||
ureq = "2"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
signal-hook = "0.3"
|
||||
|
||||
[[bin]]
|
||||
name = "mvp-worker-node"
|
||||
path = "src/bin/worker_node.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "mvp-orchestrator"
|
||||
path = "src/bin/orchestrator.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "mvp-chat"
|
||||
path = "src/bin/mvp_chat.rs"
|
||||
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
# Myelin System Specification
|
||||
# MVP System Specification
|
||||
***STALE! FOR HISTORICAL REFERENCE ONLY***
|
||||
|
||||
Id: 9
|
||||
Last modified:
|
||||
Last reviewed:
|
||||
**Status:** draft consolidated system specification.
|
||||
|
||||
This document is the single Myelin reference for the swactor GGUF pipeline system.
|
||||
This document is the single MVP reference for the swactor GGUF pipeline system.
|
||||
It folds the system behavior previously split across the orchestration,
|
||||
ring/backpressure, and GPU-worker drafts into one end-to-end specification.
|
||||
|
||||
The focused root specs may remain as design history while this document is being
|
||||
stabilized. For the Myelin drafting process, this document is the place where the
|
||||
stabilized. For the MVP drafting process, this document is the place where the
|
||||
complete system, its components, guarantees, and systemic behavior are described
|
||||
together.
|
||||
|
||||
|
|
@ -18,7 +16,7 @@ together.
|
|||
|
||||
## 1. Purpose
|
||||
|
||||
The Myelin is a trusted distributed ML runtime for one linear GGUF inference
|
||||
The MVP is a trusted distributed ML runtime for one linear GGUF inference
|
||||
pipeline over a rented pool of GPU nodes.
|
||||
|
||||
The first deployment shape is:
|
||||
|
|
@ -38,7 +36,7 @@ operator provisions rented GPU nodes
|
|||
```
|
||||
|
||||
The document is intentionally about the first complete feature, not a broad
|
||||
graph engine. Later features can generalize from this system after the Myelin
|
||||
graph engine. Later features can generalize from this system after the MVP
|
||||
behavior is stable.
|
||||
|
||||
---
|
||||
|
|
@ -102,7 +100,7 @@ no actor-level credit protocol, RTS/CTS exchange, or per-range acknowledgement.
|
|||
One persistent iroh uni-stream carries all objects for one edge. The stream
|
||||
starts with an `edge_id` preamble and then object records.
|
||||
|
||||
The Myelin permits transfer and device upload/download to overlap with network
|
||||
The MVP permits transfer and device upload/download to overlap with network
|
||||
streaming. It does not permit compute to observe a logical object until the
|
||||
whole object has loaded.
|
||||
|
||||
|
|
@ -148,7 +146,7 @@ its active run plan.
|
|||
|
||||
Other stages cannot redirect a node to a new peer, edge id, or layer range.
|
||||
|
||||
Resource inventory is known to the orchestrator before placement in the Myelin. A
|
||||
Resource inventory is known to the orchestrator before placement in the MVP. A
|
||||
node may report boot health and readiness, but there is no distributed
|
||||
capability negotiation protocol after boot.
|
||||
|
||||
|
|
@ -182,7 +180,7 @@ Python/tinygrad worker process
|
|||
|
||||
The orchestrator may run in its own container or on one of the provisioned GPU
|
||||
machines. It is a control participant and token endpoint. It does not run model
|
||||
compute for the Myelin.
|
||||
compute for the MVP.
|
||||
|
||||
When the orchestrator participates in token edges over the data plane, it has a
|
||||
stable `node_id`, an iroh endpoint, and local token edge endpoints like any
|
||||
|
|
@ -243,7 +241,7 @@ restart invalidates all prior handles, roles, rings, and in-flight steps.
|
|||
|
||||
## 7. System Lifecycle
|
||||
|
||||
The complete Myelin lifecycle is:
|
||||
The complete MVP lifecycle is:
|
||||
|
||||
```text
|
||||
Deployment
|
||||
|
|
@ -318,7 +316,7 @@ No run edge should be provisioned to that node.
|
|||
The orchestrator starts run planning only after the intended candidate pool is
|
||||
ready.
|
||||
|
||||
For the Myelin, `PoolReady` means:
|
||||
For the MVP, `PoolReady` means:
|
||||
|
||||
- every node in the intended candidate pool is known to the orchestrator
|
||||
- every candidate node is live in the SWIM membership view
|
||||
|
|
@ -336,14 +334,14 @@ membership/liveness input to the orchestrator, not a distributed graph protocol.
|
|||
If pool readiness is lost before a `RunPlan` is committed, the orchestrator keeps
|
||||
waiting or aborts before provisioning according to local policy.
|
||||
|
||||
If pool readiness is lost after provisioning begins, the run faults. The Myelin does
|
||||
If pool readiness is lost after provisioning begins, the run faults. The MVP does
|
||||
not re-place an active run after a node disappears.
|
||||
|
||||
---
|
||||
|
||||
## 10. GGUF Pipeline Workload
|
||||
|
||||
The Myelin workload is one linear pipeline:
|
||||
The MVP workload is one linear pipeline:
|
||||
|
||||
```text
|
||||
orchestrator --tokens--> stage 0 --activations--> stage 1 --activations-->
|
||||
|
|
@ -647,7 +645,7 @@ Stopped
|
|||
terminal for this run
|
||||
```
|
||||
|
||||
The Myelin allows one active `ExecuteStep` per stage.
|
||||
The MVP allows one active `ExecuteStep` per stage.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -754,7 +752,7 @@ ObjectHeader
|
|||
payload bytes, exactly header.extent bytes
|
||||
```
|
||||
|
||||
The Myelin header is fixed-size:
|
||||
The MVP header is fixed-size:
|
||||
|
||||
```rust
|
||||
struct ObjectHeader {
|
||||
|
|
@ -842,7 +840,7 @@ struct RingLayout {
|
|||
pointers from its own arena mapping base plus arena offsets.
|
||||
|
||||
The ring header and data region must be aligned for cross-process atomics and
|
||||
backend copy requirements. Host pinning is optional for the Myelin, but if a range
|
||||
backend copy requirements. Host pinning is optional for the MVP, but if a range
|
||||
is pinned, it cannot be unpinned or returned to the free list until ring
|
||||
quiescence.
|
||||
|
||||
|
|
@ -1103,7 +1101,7 @@ The worker parses object records from the ring.
|
|||
|
||||
The stream is persistent for the edge. It is not opened per object.
|
||||
|
||||
QUIC owns reliability and stream ordering. The Myelin does not add app-level
|
||||
QUIC owns reliability and stream ordering. The MVP does not add app-level
|
||||
fragment hashes, striping, resume, or per-object streams.
|
||||
|
||||
---
|
||||
|
|
@ -2012,7 +2010,7 @@ Run fault sources include:
|
|||
Teardown failures are recorded in teardown outcome/status. They do not rewrite
|
||||
a completed inference into a faulted run outcome.
|
||||
|
||||
Myelin recovery policy is fail-stop at the run level. The orchestrator records the
|
||||
MVP recovery policy is fail-stop at the run level. The orchestrator records the
|
||||
first run-level failure reason, stops injecting objects, and begins teardown. It
|
||||
does not re-place the run.
|
||||
|
||||
|
|
@ -2324,7 +2322,7 @@ stable.
|
|||
|
||||
---
|
||||
|
||||
## 42. Canonical Myelin Scenario
|
||||
## 42. Canonical MVP Scenario
|
||||
|
||||
The first end-to-end scenario is one "hello world" prompt over a rented GPU
|
||||
pool:
|
||||
3
crates/mvp-system/src/bin/mvp_chat.rs
Normal file
3
crates/mvp-system/src/bin/mvp_chat.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() -> std::process::ExitCode {
|
||||
mvp_system::run_chat_from_args(std::env::args().skip(1))
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match myelin::run_orchestrator_from_args(std::env::args().skip(1)) {
|
||||
match mvp_system::run_orchestrator_from_args(std::env::args().skip(1)) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("myelin-orchestrator: {error}");
|
||||
eprintln!("mvp-orchestrator: {error}");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
myelin::run_worker_node_from_env()
|
||||
mvp_system::run_worker_node_from_env()
|
||||
}
|
||||
4
crates/mvp-system/src/chat/mod.rs
Normal file
4
crates/mvp-system/src/chat/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
//! MVP operator chat wrapper public surface.
|
||||
|
||||
mod node_image;
|
||||
pub(super) mod runtime;
|
||||
|
|
@ -8,19 +8,19 @@ use std::thread;
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
const NODE_IMAGE_CONTENT_INPUTS: &[&str] = &[
|
||||
"apps/myelin/node-image/Dockerfile",
|
||||
"apps/myelin/node-image/tinygrad_worker.py",
|
||||
"apps/mvp-node/Dockerfile",
|
||||
"apps/mvp-node/tinygrad_worker.py",
|
||||
];
|
||||
|
||||
const BASE_IMAGE_SOURCE_INPUTS: &[&str] = &[
|
||||
"apps/myelin/node-image/Dockerfile.base",
|
||||
"apps/myelin/node-image/myelin_entrypoint.sh",
|
||||
"apps/mvp-node/Dockerfile.base",
|
||||
"apps/mvp-node/mvp_entrypoint.sh",
|
||||
];
|
||||
const NODE_IMAGE_TAG_LABEL: &str = "org.swactor.myelin.node-image-tag";
|
||||
const NODE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.myelin.node.source-hash";
|
||||
const NODE_IMAGE_WORKER_HASH_LABEL: &str = "org.swactor.myelin.node.worker-hash";
|
||||
const NODE_IMAGE_BASE_HASH_LABEL: &str = "org.swactor.myelin.node.base-hash";
|
||||
const BASE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.myelin.base.source-hash";
|
||||
const NODE_IMAGE_TAG_LABEL: &str = "org.swactor.mvp.node-image-tag";
|
||||
const NODE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.mvp.node.source-hash";
|
||||
const NODE_IMAGE_WORKER_HASH_LABEL: &str = "org.swactor.mvp.node.worker-hash";
|
||||
const NODE_IMAGE_BASE_HASH_LABEL: &str = "org.swactor.mvp.node.base-hash";
|
||||
const BASE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.mvp.base.source-hash";
|
||||
|
||||
pub(super) struct NodeImageRequest {
|
||||
pub(super) requested_image: String,
|
||||
|
|
@ -110,11 +110,11 @@ fn prepare_node_image_inner(
|
|||
"build",
|
||||
"--quiet",
|
||||
"-p",
|
||||
"myelin",
|
||||
"mvp-system",
|
||||
"--bin",
|
||||
"myelin-worker",
|
||||
"mvp-worker-node",
|
||||
],
|
||||
"build myelin-worker",
|
||||
"build mvp-worker-node",
|
||||
None,
|
||||
progress,
|
||||
)?;
|
||||
|
|
@ -128,7 +128,7 @@ fn prepare_node_image_inner(
|
|||
&root,
|
||||
vec![relative_path(
|
||||
&root,
|
||||
&root.join("apps/myelin/node-image/tinygrad_worker.py"),
|
||||
&root.join("apps/mvp-node/tinygrad_worker.py"),
|
||||
)?],
|
||||
&[],
|
||||
)?;
|
||||
|
|
@ -176,14 +176,14 @@ fn prepare_node_image_inner(
|
|||
&[
|
||||
"build",
|
||||
"-f",
|
||||
"apps/myelin/node-image/Dockerfile.base",
|
||||
"apps/mvp-node/Dockerfile.base",
|
||||
"--label",
|
||||
base_source_hash_label.as_str(),
|
||||
"-t",
|
||||
request.base_image.as_str(),
|
||||
".",
|
||||
],
|
||||
"build myelin node base image",
|
||||
"build mvp node base image",
|
||||
Some(&request.base_image),
|
||||
progress,
|
||||
)?;
|
||||
|
|
@ -200,11 +200,11 @@ fn prepare_node_image_inner(
|
|||
let mut build_args = vec![
|
||||
"build".to_owned(),
|
||||
"-f".to_owned(),
|
||||
"apps/myelin/node-image/Dockerfile".to_owned(),
|
||||
"apps/mvp-node/Dockerfile".to_owned(),
|
||||
"--build-arg".to_owned(),
|
||||
format!("BASE_IMAGE={}", request.base_image),
|
||||
"--build-arg".to_owned(),
|
||||
format!("MYELIN_NODE_BIN={node_bin}"),
|
||||
format!("MVP_NODE_BIN={node_bin}"),
|
||||
];
|
||||
for (key, value) in &expected_node_labels {
|
||||
build_args.push("--label".to_owned());
|
||||
|
|
@ -215,7 +215,7 @@ fn prepare_node_image_inner(
|
|||
&root,
|
||||
"docker",
|
||||
&build_args.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||
"build myelin node image",
|
||||
"build mvp node image",
|
||||
Some(&image_ref),
|
||||
progress,
|
||||
)?;
|
||||
|
|
@ -437,7 +437,7 @@ fn ensure_aliases_local(
|
|||
root,
|
||||
"docker",
|
||||
&["tag", source_ref, &alias],
|
||||
"tag myelin node image",
|
||||
"tag mvp node image",
|
||||
Some(&alias),
|
||||
progress,
|
||||
)?;
|
||||
|
|
@ -461,7 +461,7 @@ fn ensure_aliases_for_remote(
|
|||
root,
|
||||
"docker",
|
||||
&["pull", source_ref],
|
||||
"pull myelin node image",
|
||||
"pull mvp node image",
|
||||
Some(source_ref),
|
||||
progress,
|
||||
)?;
|
||||
|
|
@ -489,7 +489,7 @@ fn push_image(
|
|||
root,
|
||||
"docker",
|
||||
&["push", image_ref],
|
||||
"push myelin node image",
|
||||
"push mvp node image",
|
||||
Some(image_ref),
|
||||
progress,
|
||||
)
|
||||
|
|
@ -509,7 +509,7 @@ fn docker_image_labels_match(
|
|||
}
|
||||
|
||||
fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
|
||||
let prune_enabled = std::env::var("MYELIN_NODE_IMAGE_PRUNE")
|
||||
let prune_enabled = std::env::var("MVP_NODE_IMAGE_PRUNE")
|
||||
.map(|value| {
|
||||
let value = value.trim().to_ascii_lowercase();
|
||||
!matches!(value.as_str(), "0" | "false" | "no" | "off")
|
||||
|
|
@ -522,12 +522,12 @@ fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
|
|||
let tags = match docker_image_tags(root, &image.repository) {
|
||||
Ok(tags) => tags,
|
||||
Err(error) => {
|
||||
eprintln!("myelin-node-image: prune old dirty images skipped: {error}");
|
||||
eprintln!("mvp-node-image: prune old dirty images skipped: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let keep_old = std::env::var("MYELIN_NODE_IMAGE_PRUNE_KEEP")
|
||||
let keep_old = std::env::var("MVP_NODE_IMAGE_PRUNE_KEEP")
|
||||
.ok()
|
||||
.and_then(|value| value.trim().parse::<usize>().ok())
|
||||
.unwrap_or(3);
|
||||
|
|
@ -554,7 +554,7 @@ fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
|
|||
}
|
||||
if docker_image_has_container(root, &image_ref) {
|
||||
eprintln!(
|
||||
"myelin-node-image: prune old dirty image {image_ref} skipped: container exists"
|
||||
"mvp-node-image: prune old dirty image {image_ref} skipped: container exists"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -563,9 +563,9 @@ fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
|
|||
continue;
|
||||
}
|
||||
|
||||
eprintln!("myelin-node-image: prune old dirty image {image_ref}");
|
||||
eprintln!("mvp-node-image: prune old dirty image {image_ref}");
|
||||
if let Err(error) = docker_image_remove(root, &image_ref) {
|
||||
eprintln!("myelin-node-image: prune old dirty image {image_ref} skipped: {error}");
|
||||
eprintln!("mvp-node-image: prune old dirty image {image_ref} skipped: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -669,7 +669,7 @@ fn run_status_command(
|
|||
progress: &mut Option<&mut dyn NodeImageProgressSink>,
|
||||
) -> Result<(), String> {
|
||||
let args: Vec<String> = args.iter().map(|arg| (*arg).to_owned()).collect();
|
||||
eprintln!("myelin-node-image: {label}");
|
||||
eprintln!("mvp-node-image: {label}");
|
||||
if progress.is_none() {
|
||||
let status = Command::new(program)
|
||||
.current_dir(root)
|
||||
|
|
@ -38,14 +38,14 @@ use iroh_driver::EndpointAddrMask;
|
|||
|
||||
const DEFAULT_CONFIG_PATH: &str = ".config/config.toml";
|
||||
const DEFAULT_RPC_ADDR: &str = "127.0.0.1:19777";
|
||||
const BASE_NODE_IMAGE: &str = "myelin-node-base:cuda12.6";
|
||||
const BASE_NODE_IMAGE: &str = "swactor-mvp-node-base:cuda12.6";
|
||||
const REPO_MODEL_CACHE_DIR: &str = ".model-cache";
|
||||
const DEFAULT_MAX_TOKENS: u32 = 64;
|
||||
const ORCH_SHUTDOWN_GRACE_MS: u64 = 5_000;
|
||||
const VASTAI_ORCH_SHUTDOWN_GRACE_MS: u64 = 180_000;
|
||||
const MYELIN_CHAT_GPU_RUN_ENV: &str = "MYELIN_CHAT_GPU_RUN";
|
||||
const MYELIN_CHAT_USAGE: &str = "\
|
||||
USAGE: cargo myelin-chat [OPTIONS]
|
||||
const MVP_CHAT_GPU_RUN_ENV: &str = "MVP_CHAT_GPU_RUN";
|
||||
const MVP_CHAT_USAGE: &str = "\
|
||||
USAGE: cargo mvp-chat [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--gpu Run the local GPU path: in-process orchestrator plus DEV=CUDA worker selection
|
||||
|
|
@ -54,20 +54,20 @@ OPTIONS:
|
|||
--config <path> Load config overlay
|
||||
--pipeline-stages|--pipeline-parallel <count>
|
||||
--relay-mode <mode> Relay mode: default or disabled
|
||||
--relay-url <url> Custom relay URL passed to myelin-orchestrator
|
||||
--relay-url <url> Custom relay URL passed to mvp-orchestrator
|
||||
--endpoint-addr-mask <mask> Endpoint address mask: full or relay-only
|
||||
--cached-model[=<path>] Use discovered or explicit cached GGUF model (default for --process)
|
||||
--cached-model[=<path>] Use discovered or explicit cached GGUF model
|
||||
--dump-logs[=<path>] Write datastream frame log
|
||||
--run-id <id> Override run id
|
||||
--skip-rebuild Reuse existing Cargo artifacts
|
||||
--yes, -y Approve Vast.ai lease prompts
|
||||
--help, -h Print this help";
|
||||
const ORCH_SHUTDOWN_POLL_MS: u64 = 50;
|
||||
const CHAT_LIFECYCLE_CHANNEL: &str = "myelin.chat.lifecycle";
|
||||
const CHAT_RUNTIME_CHANNEL: &str = "myelin.chat.runtime";
|
||||
const CHAT_PROMPT_CHANNEL: &str = "myelin.chat.prompt";
|
||||
const CHAT_COMPONENT_CHANNEL: &str = "myelin.chat.component";
|
||||
const CHAT_BENCHMARK_CHANNEL: &str = "myelin.chat.benchmark";
|
||||
const CHAT_LIFECYCLE_CHANNEL: &str = "mvp.chat.lifecycle";
|
||||
const CHAT_RUNTIME_CHANNEL: &str = "mvp.chat.runtime";
|
||||
const CHAT_PROMPT_CHANNEL: &str = "mvp.chat.prompt";
|
||||
const CHAT_COMPONENT_CHANNEL: &str = "mvp.chat.component";
|
||||
const CHAT_BENCHMARK_CHANNEL: &str = "mvp.chat.benchmark";
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PromptInput {
|
||||
|
|
@ -86,7 +86,7 @@ where
|
|||
match install_signal_handlers().and_then(|()| run(args)) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("myelin-chat: {error}");
|
||||
eprintln!("mvp-chat: {error}");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +129,7 @@ where
|
|||
.iter()
|
||||
.any(|arg| matches!(arg.as_str(), "--help" | "-h" | "help"))
|
||||
{
|
||||
println!("{MYELIN_CHAT_USAGE}");
|
||||
println!("{MVP_CHAT_USAGE}");
|
||||
return Ok(());
|
||||
}
|
||||
let config = Config::from_args(provided_args)?;
|
||||
|
|
@ -386,11 +386,11 @@ struct ChatDatastream {
|
|||
|
||||
impl ChatDatastream {
|
||||
fn new(run_id: u64, archive_path: Option<PathBuf>) -> Result<Self, String> {
|
||||
let stream = StreamId::new(NodeId::new("myelin-chat"), Lifetime(run_id));
|
||||
let stream = StreamId::new(NodeId::new("mvp-chat"), Lifetime(run_id));
|
||||
let endpoint = DatastreamEndpoint::with_descriptor(
|
||||
StreamDescriptor {
|
||||
stream: stream.clone(),
|
||||
label: Some("myelin chat".to_owned()),
|
||||
label: Some("mvp chat".to_owned()),
|
||||
origin: StreamOrigin::Orchestrator,
|
||||
},
|
||||
1024,
|
||||
|
|
@ -436,7 +436,7 @@ impl ChatDatastream {
|
|||
|
||||
fn emit(&mut self, channel: &str, phase: &str, status: &str, detail: Value) {
|
||||
let id = self.channel_by_name(channel);
|
||||
let benchmark = benchmark::stamp("myelin-chat");
|
||||
let benchmark = benchmark::stamp("mvp-chat");
|
||||
let payload = serde_json::to_vec(&json!({
|
||||
"schema_version": benchmark["schema_version"].clone(),
|
||||
"type": "ChatProgress",
|
||||
|
|
@ -452,19 +452,19 @@ impl ChatDatastream {
|
|||
"wall_clock_unix_ms": benchmark["wall_clock_unix_ms"].clone(),
|
||||
"monotonic_ms": benchmark["monotonic_ms"].clone(),
|
||||
"clock_source": benchmark["clock_source"].clone(),
|
||||
"span_id": format!("myelin-chat:{}:{}:{phase}", self.run_id, benchmark["producer_sequence"]),
|
||||
"span_id": format!("mvp-chat:{}:{}:{phase}", self.run_id, benchmark["producer_sequence"]),
|
||||
"parent_span_id": Value::Null,
|
||||
"benchmark": benchmark,
|
||||
"detail": detail,
|
||||
}))
|
||||
.expect("serialize myelin-chat progress event");
|
||||
.expect("serialize mvp-chat progress event");
|
||||
self.producer.submit_bytes(id, payload);
|
||||
self.flush();
|
||||
}
|
||||
|
||||
fn emit_benchmark_envelope(&mut self, config: &Config) {
|
||||
let id = self.channel_by_name(CHAT_BENCHMARK_CHANNEL);
|
||||
let benchmark = benchmark::stamp("myelin-chat");
|
||||
let benchmark = benchmark::stamp("mvp-chat");
|
||||
let payload = serde_json::to_vec(&json!({
|
||||
"schema_version": benchmark["schema_version"].clone(),
|
||||
"type": "BenchmarkRunEnvelope",
|
||||
|
|
@ -480,11 +480,11 @@ impl ChatDatastream {
|
|||
"wall_clock_unix_ms": benchmark["wall_clock_unix_ms"].clone(),
|
||||
"monotonic_ms": benchmark["monotonic_ms"].clone(),
|
||||
"clock_source": benchmark["clock_source"].clone(),
|
||||
"span_id": format!("myelin-chat:{}:{}:run_envelope", self.run_id, benchmark["producer_sequence"]),
|
||||
"span_id": format!("mvp-chat:{}:{}:run_envelope", self.run_id, benchmark["producer_sequence"]),
|
||||
"parent_span_id": Value::Null,
|
||||
"benchmark": benchmark,
|
||||
"detail": {
|
||||
"scenario": "myelin-chat",
|
||||
"scenario": "mvp-chat",
|
||||
"detail_level": "benchmark_observability_v1",
|
||||
"workload": {
|
||||
"mode": "stdin_prompt_corpus",
|
||||
|
|
@ -533,7 +533,7 @@ impl ChatDatastream {
|
|||
},
|
||||
},
|
||||
}))
|
||||
.expect("serialize myelin-chat benchmark envelope");
|
||||
.expect("serialize mvp-chat benchmark envelope");
|
||||
self.producer.submit_bytes(id, payload);
|
||||
self.flush();
|
||||
}
|
||||
|
|
@ -551,7 +551,7 @@ impl ChatDatastream {
|
|||
"relay_configured": config.relay_url.is_some(),
|
||||
"endpoint_addr_mask": config.endpoint_addr_mask.as_str(),
|
||||
});
|
||||
let synthetic_id = format!("myelin-chat-{}-datastream-preflight", self.run_id);
|
||||
let synthetic_id = format!("mvp-chat-{}-datastream-preflight", self.run_id);
|
||||
for (phase, status) in [
|
||||
("DatastreamProducerConfigured", "configured"),
|
||||
("DatastreamProducerConnected", "ready"),
|
||||
|
|
@ -563,7 +563,7 @@ impl ChatDatastream {
|
|||
phase,
|
||||
status,
|
||||
json!({
|
||||
"producer": "myelin-chat",
|
||||
"producer": "mvp-chat",
|
||||
"producer_class": "rust-chat",
|
||||
"synthetic_id": synthetic_id,
|
||||
"datastream_endpoint": endpoint,
|
||||
|
|
@ -576,8 +576,8 @@ impl ChatDatastream {
|
|||
"endpoint_config_snapshot",
|
||||
"ready",
|
||||
json!({
|
||||
"producer": "myelin-chat",
|
||||
"expected_producers": ["myelin-chat", "myelin-orchestrator", "myelin-worker", "tinygrad-worker"],
|
||||
"producer": "mvp-chat",
|
||||
"expected_producers": ["mvp-chat", "mvp-orchestrator", "mvp-worker-node", "tinygrad-worker"],
|
||||
"datastream_endpoint": endpoint,
|
||||
"runtime_endpoint": runtime_endpoint,
|
||||
"connectivity_preflight": {
|
||||
|
|
@ -597,7 +597,7 @@ impl ChatDatastream {
|
|||
.cloned()
|
||||
.unwrap_or_else(|| format!("channel#{}", frame.channel.0));
|
||||
self.pending
|
||||
.push(("myelin-chat".to_owned(), stream.clone(), channel, frame));
|
||||
.push(("mvp-chat".to_owned(), stream.clone(), channel, frame));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -609,7 +609,7 @@ impl ChatDatastream {
|
|||
if self.pending.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut archive = FrameArchive::open_with_label(path, "myelin-chat datastream frame log")?;
|
||||
let mut archive = FrameArchive::open_with_label(path, "mvp-chat datastream frame log")?;
|
||||
for (source, stream, channel, frame) in self.pending.drain(..) {
|
||||
archive.record(&source, &stream, &channel, &frame)?;
|
||||
}
|
||||
|
|
@ -790,18 +790,12 @@ impl Config {
|
|||
}
|
||||
let pipeline_stages = Self::pipeline_stages(&args, &toml)?;
|
||||
let max_tokens = Self::max_tokens(&toml)?;
|
||||
let gpu_run = args.gpu || env_flag(MYELIN_CHAT_GPU_RUN_ENV, false);
|
||||
let gpu_run = args.gpu || env_flag(MVP_CHAT_GPU_RUN_ENV, false);
|
||||
let endpoint_addr_mask = Self::endpoint_addr_mask(&args, &toml)?;
|
||||
let (relay_mode, relay_url) = Self::relay_settings(&args, &toml, endpoint_addr_mask)?;
|
||||
let cached_model = match Self::cached_model_source(&args, &provider) {
|
||||
None => None,
|
||||
Some(CachedModelSource::Path(path)) => Some(CachedModelConfig::from_path(path)?),
|
||||
Some(CachedModelSource::Discover) => match CachedModelConfig::discover() {
|
||||
Ok(config) => Some(config),
|
||||
Err(error) if args.cached_model.is_some() => return Err(error),
|
||||
Err(_) => None,
|
||||
},
|
||||
};
|
||||
let cached_model = Self::cached_model_source(&args, gpu_run, &provider)
|
||||
.map(CachedModelConfig::from_source)
|
||||
.transpose()?;
|
||||
let model = Self::model_config(&provider, &toml, cached_model.as_ref())?;
|
||||
let datastream_frame_log = Self::datastream_frame_log(&args, &toml);
|
||||
let vastai = if provider == provider_kind::vastai() {
|
||||
|
|
@ -811,7 +805,7 @@ impl Config {
|
|||
};
|
||||
|
||||
Ok(Self {
|
||||
orch_bin: artifact_root().join("target/debug/myelin-orchestrator"),
|
||||
orch_bin: artifact_root().join("target/debug/mvp-orchestrator"),
|
||||
worker_bin: default_worker_bin(),
|
||||
rpc_addr: DEFAULT_RPC_ADDR.to_owned(),
|
||||
node_image,
|
||||
|
|
@ -882,15 +876,16 @@ impl Config {
|
|||
Ok((relay_mode, relay_url))
|
||||
}
|
||||
|
||||
/// Resolve the cached-model source. The process provider defaults to
|
||||
/// best-effort discovery of `.model-cache/` so `cargo myelin-chat` runs a
|
||||
/// cached GGUF model without explicit flags. Explicit `--cached-model` is
|
||||
/// always honored (and stays strict); the default degrades gracefully to
|
||||
/// the normal download path when no cached model is present.
|
||||
fn cached_model_source(args: &ParsedArgs, provider: &ProviderKind) -> Option<CachedModelSource> {
|
||||
fn cached_model_source(
|
||||
args: &ParsedArgs,
|
||||
gpu_run: bool,
|
||||
provider: &ProviderKind,
|
||||
) -> Option<CachedModelSource> {
|
||||
match &args.cached_model {
|
||||
Some(source) => Some(source.clone()),
|
||||
None if provider == &provider_kind::process() => Some(CachedModelSource::Discover),
|
||||
None if gpu_run && provider == &provider_kind::process() => {
|
||||
Some(CachedModelSource::Discover)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -916,7 +911,7 @@ impl Config {
|
|||
return Some(
|
||||
args.dump_log_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("myelin-chat.log")),
|
||||
.unwrap_or_else(|| PathBuf::from("mvp-chat.log")),
|
||||
);
|
||||
}
|
||||
if !toml.observability.dump_logs.unwrap_or(false) {
|
||||
|
|
@ -925,7 +920,7 @@ impl Config {
|
|||
Some(
|
||||
first_non_empty([toml.observability.dump_log_path.clone()])
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("myelin-chat.log")),
|
||||
.unwrap_or_else(|| PathBuf::from("mvp-chat.log")),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1196,7 +1191,7 @@ impl ParsedArgs {
|
|||
{
|
||||
continue;
|
||||
}
|
||||
return Err(format!("unsupported myelin-chat argument {arg:?}"));
|
||||
return Err(format!("unsupported mvp-chat argument {arg:?}"));
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
|
@ -1655,16 +1650,16 @@ where
|
|||
true,
|
||||
config.skip_rebuild,
|
||||
&config.orch_bin,
|
||||
"myelin-orchestrator",
|
||||
"mvp-orchestrator",
|
||||
&[
|
||||
"build",
|
||||
"--quiet",
|
||||
"-p",
|
||||
"myelin",
|
||||
"mvp-system",
|
||||
"--features",
|
||||
"dashboard",
|
||||
"--bin",
|
||||
"myelin-orchestrator",
|
||||
"mvp-orchestrator",
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
|
@ -1677,14 +1672,14 @@ where
|
|||
false,
|
||||
config.skip_rebuild,
|
||||
&config.worker_bin,
|
||||
"myelin-worker",
|
||||
"mvp-worker-node",
|
||||
&[
|
||||
"build",
|
||||
"--quiet",
|
||||
"-p",
|
||||
"myelin",
|
||||
"mvp-system",
|
||||
"--bin",
|
||||
"myelin-worker",
|
||||
"mvp-worker-node",
|
||||
],
|
||||
)?;
|
||||
emit_chat_progress(
|
||||
|
|
@ -1722,14 +1717,14 @@ where
|
|||
true,
|
||||
config.skip_rebuild,
|
||||
&config.worker_bin,
|
||||
"myelin-worker",
|
||||
"mvp-worker-node",
|
||||
&[
|
||||
"build",
|
||||
"--quiet",
|
||||
"-p",
|
||||
"myelin",
|
||||
"mvp-system",
|
||||
"--bin",
|
||||
"myelin-worker",
|
||||
"mvp-worker-node",
|
||||
],
|
||||
)?;
|
||||
emit_chat_progress(
|
||||
|
|
@ -2067,7 +2062,7 @@ fn run_chat_session_with_output_and_progress(
|
|||
}
|
||||
|
||||
fn default_worker_bin() -> PathBuf {
|
||||
artifact_root().join("target/debug/myelin-worker")
|
||||
artifact_root().join("target/debug/mvp-worker-node")
|
||||
}
|
||||
|
||||
fn ensure_runtime_binary(
|
||||
|
|
@ -2127,6 +2122,13 @@ struct CachedModelConfig {
|
|||
}
|
||||
|
||||
impl CachedModelConfig {
|
||||
fn from_source(source: CachedModelSource) -> Result<Self, String> {
|
||||
match source {
|
||||
CachedModelSource::Discover => Self::discover(),
|
||||
CachedModelSource::Path(path) => Self::from_path(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_path(path: PathBuf) -> Result<Self, String> {
|
||||
let metadata = fs::metadata(&path)
|
||||
.map_err(|e| format!("stat cached model {}: {e}", path.display()))?;
|
||||
|
|
@ -2263,9 +2265,9 @@ mod tests {
|
|||
|
||||
const PROCESS_ENV_KEYS: &[&str] = &[
|
||||
"VAST_API_KEY",
|
||||
"MYELIN_PIPELINE_STAGES",
|
||||
"MYELIN_RUNTIME_CONFIG",
|
||||
"MYELIN_CHAT_GPU_RUN",
|
||||
"MVP_PIPELINE_STAGES",
|
||||
"MVP_RUNTIME_CONFIG",
|
||||
"MVP_CHAT_GPU_RUN",
|
||||
"DEV",
|
||||
];
|
||||
|
||||
|
|
@ -2277,7 +2279,7 @@ mod tests {
|
|||
fn new(label: &str) -> Self {
|
||||
let id = NEXT_TEMP_ID.fetch_add(1, AtomicOrdering::SeqCst);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"myelin-chat-test-{}-{}-{}",
|
||||
"mvp-chat-test-{}-{}-{}",
|
||||
std::process::id(),
|
||||
id,
|
||||
label
|
||||
|
|
@ -2462,8 +2464,8 @@ mod tests {
|
|||
|
||||
with_process_state(
|
||||
&[
|
||||
("MYELIN_PIPELINE_STAGES", Some("9")),
|
||||
("MYELIN_RUNTIME_CONFIG", Some("local")),
|
||||
("MVP_PIPELINE_STAGES", Some("9")),
|
||||
("MVP_RUNTIME_CONFIG", Some("local")),
|
||||
],
|
||||
Some(temp.path()),
|
||||
|| {
|
||||
|
|
@ -2515,34 +2517,6 @@ tag = " alias "
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_defaults_use_cached_model_for_process_when_present() {
|
||||
let temp = TempDir::new("cached-model-default");
|
||||
|
||||
with_process_state(&[], Some(temp.path()), || {
|
||||
let cache_dir = temp.path().join(REPO_MODEL_CACHE_DIR);
|
||||
fs::create_dir_all(&cache_dir).expect("create model cache dir");
|
||||
fs::write(
|
||||
cache_dir.join(DEFAULT_PIPELINE_CACHED_MODEL_FILE),
|
||||
Vec::<u8>::new(),
|
||||
)
|
||||
.expect("seed cached model file");
|
||||
|
||||
let defaults = Config::from_args(Vec::<String>::new()).expect("defaults resolve");
|
||||
|
||||
assert_eq!(defaults.provider, provider_kind::process());
|
||||
let cached_model = defaults
|
||||
.cached_model
|
||||
.expect("process provider discovers a cached model by default");
|
||||
assert!(
|
||||
cached_model
|
||||
.host_path
|
||||
.ends_with(DEFAULT_PIPELINE_CACHED_MODEL_FILE),
|
||||
"discovered the seeded cached model"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_max_tokens_drives_orchestrator_args_and_submit_prompt() {
|
||||
let temp = TempDir::new("config-max-tokens");
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
//! Registers wire codecs for every Myelin actor message type.
|
||||
//! Registers wire codecs for every MVP actor message type.
|
||||
//!
|
||||
//! The runtime's [`CodecRegistry`](swactor_transport::CodecRegistry) needs an
|
||||
//! encoder/decoder entry for each inter-node message type. This is the single
|
||||
//! aggregator that wires up the node, orchestrator, prompt, and datastream
|
||||
//! publisher codecs.
|
||||
|
||||
pub(crate) fn register_myelin_actor_codecs(registry: &mut swactor_transport::CodecRegistry) {
|
||||
pub(crate) fn register_mvp_actor_codecs(registry: &mut swactor_transport::CodecRegistry) {
|
||||
crate::node_actor::register_codecs(registry);
|
||||
crate::orchestration::actor::register_codecs(registry);
|
||||
datastream::register_datastream_publisher_codec(registry);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#![recursion_limit = "256"]
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate self as myelin;
|
||||
extern crate self as mvp_system;
|
||||
|
||||
const DEFAULT_PIPELINE_CACHED_MODEL_FILE: &str = "SmolLM2-135M-Instruct.Q4_0.gguf";
|
||||
const DEFAULT_PIPELINE_CACHED_MODEL_REPO: &str = "QuantFactory/SmolLM2-135M-Instruct-GGUF";
|
||||
|
|
@ -170,19 +170,6 @@ pub(crate) enum NodeAgentMsg {
|
|||
WorkerCrashed {
|
||||
reason: Option<String>,
|
||||
},
|
||||
StepFailed {
|
||||
step_id: u64,
|
||||
},
|
||||
ObjectFailed {
|
||||
edge_id: u64,
|
||||
object_id: Option<u64>,
|
||||
},
|
||||
OutputFault {
|
||||
edge_id: u64,
|
||||
},
|
||||
EdgeFault {
|
||||
edge_id: u64,
|
||||
},
|
||||
StopRun {
|
||||
run_id: u64,
|
||||
},
|
||||
|
|
@ -221,7 +208,7 @@ pub(crate) enum NodeAgentMsg {
|
|||
|
||||
impl NetworkMessage for NodeAgentMsg {
|
||||
fn type_tag() -> &'static str {
|
||||
"myelin::NodeAgentMsg"
|
||||
"mvp_system::NodeAgentMsg"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -249,6 +236,9 @@ pub(crate) enum StageCommandWire {
|
|||
layer_end_exclusive: u32,
|
||||
stage_shard_plan: Option<StageShardPlan>,
|
||||
},
|
||||
RewireEdge {
|
||||
edge_id: u64,
|
||||
},
|
||||
ExecuteStep {
|
||||
step_id: u64,
|
||||
input_edge_id: u64,
|
||||
|
|
@ -324,7 +314,7 @@ pub(crate) enum NodeAgentReport {
|
|||
|
||||
impl NetworkMessage for NodeAgentReport {
|
||||
fn type_tag() -> &'static str {
|
||||
"myelin::NodeAgentReport"
|
||||
"mvp_system::NodeAgentReport"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -541,27 +531,6 @@ impl NodeAgentActor {
|
|||
self.last_worker_crash = reason;
|
||||
self.core.observe(stage::StageEvent::WorkerCrashed)
|
||||
}
|
||||
NodeAgentMsg::StepFailed { step_id } => {
|
||||
self.core.observe(stage::StageEvent::StepFailed {
|
||||
step_id: stage::StepId(step_id),
|
||||
})
|
||||
}
|
||||
NodeAgentMsg::ObjectFailed { edge_id, object_id } => {
|
||||
self.core.observe(stage::StageEvent::ObjectFailed {
|
||||
edge_id: stage::EdgeId(edge_id),
|
||||
object_id: object_id.map(stage::ObjectId),
|
||||
})
|
||||
}
|
||||
NodeAgentMsg::OutputFault { edge_id } => {
|
||||
self.core.observe(stage::StageEvent::OutputFault {
|
||||
edge_id: stage::EdgeId(edge_id),
|
||||
})
|
||||
}
|
||||
NodeAgentMsg::EdgeFault { edge_id } => {
|
||||
self.core.observe(stage::StageEvent::EdgeFault {
|
||||
edge_id: stage::EdgeId(edge_id),
|
||||
})
|
||||
}
|
||||
NodeAgentMsg::StopRun { run_id } => self.core.observe(stage::StageEvent::StopRun {
|
||||
run_id: stage::RunId(run_id),
|
||||
}),
|
||||
|
|
@ -723,6 +692,7 @@ impl From<&stage::StageCommand> for StageCommandWire {
|
|||
layer_end_exclusive: range.end_exclusive,
|
||||
stage_shard_plan: shard_plan.clone(),
|
||||
},
|
||||
stage::StageCommand::RewireEdge { edge_id } => Self::RewireEdge { edge_id: edge_id.0 },
|
||||
stage::StageCommand::ExecuteStep(step) => Self::ExecuteStep {
|
||||
step_id: step.step_id.0,
|
||||
input_edge_id: step.input.edge_id.0,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! Myelin node-local runtime public surface.
|
||||
//! MVP node-local runtime public surface.
|
||||
//!
|
||||
//! Worker-node runtime behavior lives behind this module boundary; binaries
|
||||
//! only wire entrypoints into it.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//! Worker-node runtime behavior behind the `myelin::node` boundary.
|
||||
//! Worker-node runtime behavior behind the `mvp_system::node` boundary.
|
||||
//!
|
||||
//! The `myelin-worker` binary remains a thin entrypoint wrapper; this module
|
||||
//! The `mvp-worker-node` binary remains a thin entrypoint wrapper; this module
|
||||
//! owns the reusable node-local runtime, helper-command, datastream archive,
|
||||
//! debug-join, staging, transport, and worker coordination behavior.
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ use datastream::{
|
|||
Lifetime, NodeId, Record, StreamDescriptor, StreamId, StreamOrigin,
|
||||
};
|
||||
|
||||
use crate::codecs::register_myelin_actor_codecs;
|
||||
use crate::codecs::register_mvp_actor_codecs;
|
||||
use crate::gguf_shard::{StageShardPlan, materialize_stage_shard_http, validate_stage_shard_cache};
|
||||
use crate::node_actor::{
|
||||
NodeAgentActor, NodeAgentMsg, NodeAgentReport, StageCommandWire, StageInboundEdgeWire,
|
||||
|
|
@ -53,7 +53,7 @@ use swactor::actor::{ActorAddress, ActorInterface};
|
|||
use swactor::runtime::{Ctx, ExternalSender};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
||||
|
||||
const DEFAULT_WORKER_SCRIPT: &str = "/usr/local/share/myelin/tinygrad_worker.py";
|
||||
const DEFAULT_WORKER_SCRIPT: &str = "/usr/local/share/mvp/tinygrad_worker.py";
|
||||
const DEFAULT_DEVICE: &str = "CUDA";
|
||||
const DEFAULT_HF_REPO: &str = "bartowski/Llama-3.2-1B-Instruct-GGUF";
|
||||
const DEFAULT_HF_FILE: &str = "Llama-3.2-1B-Instruct-Q4_K_M.gguf";
|
||||
|
|
@ -63,22 +63,22 @@ const DEFAULT_ARENA_ALIGNMENT: u64 = 64;
|
|||
const PUMP_INTERVAL: Duration = Duration::from_millis(10);
|
||||
const RUNTIME_READY_RETRY_INITIAL: Duration = Duration::from_millis(100);
|
||||
const RUNTIME_READY_RETRY_MAX: Duration = Duration::from_secs(2);
|
||||
const NODE_BOOTSTRAP_CHANNEL: &str = "myelin.node.bootstrap";
|
||||
const NODE_RUNTIME_CHANNEL: &str = "myelin.node.runtime";
|
||||
const NODE_STAGE_CHANNEL: &str = "myelin.node.stage";
|
||||
const NODE_WORKER_CHANNEL: &str = "myelin.node.worker";
|
||||
const NODE_PROMPT_CHANNEL: &str = "myelin.node.prompt";
|
||||
const NODE_SHUTDOWN_CHANNEL: &str = "myelin.node.shutdown";
|
||||
const NODE_SAMPLER_CHANNEL: &str = "myelin.node.sampler";
|
||||
const NODE_BOOTSTRAP_CHANNEL: &str = "mvp.node.bootstrap";
|
||||
const NODE_RUNTIME_CHANNEL: &str = "mvp.node.runtime";
|
||||
const NODE_STAGE_CHANNEL: &str = "mvp.node.stage";
|
||||
const NODE_WORKER_CHANNEL: &str = "mvp.node.worker";
|
||||
const NODE_PROMPT_CHANNEL: &str = "mvp.node.prompt";
|
||||
const NODE_SHUTDOWN_CHANNEL: &str = "mvp.node.shutdown";
|
||||
const NODE_SAMPLER_CHANNEL: &str = "mvp.node.sampler";
|
||||
const WORKER_COMMAND_WAIT_TELEMETRY_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
fn worker_benchmark_stamp(run_id: u64, node_id: u64) -> Value {
|
||||
let mut benchmark = benchmark::stamp("myelin-worker");
|
||||
let mut benchmark = benchmark::stamp("mvp-worker-node");
|
||||
let pid = benchmark
|
||||
.get("producer_process_id")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or_else(|| u64::from(std::process::id()));
|
||||
let producer_instance_id = format!("myelin-worker:{run_id}:node-{node_id}:pid-{pid}");
|
||||
let producer_instance_id = format!("mvp-worker-node:{run_id}:node-{node_id}:pid-{pid}");
|
||||
if let Some(object) = benchmark.as_object_mut() {
|
||||
object.insert(
|
||||
"producer_instance_id".to_owned(),
|
||||
|
|
@ -112,7 +112,7 @@ fn node_event_payload(
|
|||
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
|
||||
"monotonic_ms":benchmark["monotonic_ms"].clone(),
|
||||
"clock_source":benchmark["clock_source"].clone(),
|
||||
"span_id":format!("myelin-worker:{}:{}:{}:{phase}", config.run_id, config.logical_node_id, benchmark["producer_sequence"]),
|
||||
"span_id":format!("mvp-worker-node:{}:{}:{}:{phase}", config.run_id, config.logical_node_id, benchmark["producer_sequence"]),
|
||||
"parent_span_id":Value::Null,
|
||||
"benchmark":benchmark,
|
||||
"detail":detail,
|
||||
|
|
@ -123,7 +123,7 @@ fn emit_stdio_datastream_frame(channel: &str, payload: &Value) -> Result<(), Str
|
|||
println!(
|
||||
"{}",
|
||||
json!({
|
||||
"myelin_stdio_event":1,
|
||||
"mvp_stdio_event":1,
|
||||
"kind":"datastream_frame",
|
||||
"channel":channel,
|
||||
"payload":payload,
|
||||
|
|
@ -200,7 +200,7 @@ fn debug_join_client_main(args: Vec<String>) -> ExitCode {
|
|||
match serde_json::to_string(&response) {
|
||||
Ok(line) => println!("{line}"),
|
||||
Err(error) => {
|
||||
eprintln!("myelin-worker debug-join: serialize response: {error}");
|
||||
eprintln!("mvp-worker-node debug-join: serialize response: {error}");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -211,11 +211,11 @@ fn debug_join_client_main(args: Vec<String>) -> ExitCode {
|
|||
}
|
||||
}
|
||||
Err(DebugJoinClientError::Cli(error)) => {
|
||||
eprintln!("myelin-worker debug-join: {error}");
|
||||
eprintln!("mvp-worker-node debug-join: {error}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
Err(DebugJoinClientError::Runtime(error)) => {
|
||||
eprintln!("myelin-worker debug-join: {error}");
|
||||
eprintln!("mvp-worker-node debug-join: {error}");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -339,7 +339,7 @@ fn spawn_debug_join_listener(
|
|||
});
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("myelin-worker debug join listener stopped: {error}");
|
||||
eprintln!("mvp-worker-node debug join listener stopped: {error}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -483,7 +483,7 @@ fn sampler_health_payload(
|
|||
"type":"SamplerHealth",
|
||||
"event_type":"SamplerHealth",
|
||||
"event_name":"host_sampler_health",
|
||||
"schema":"myelin.node.sampler.health.v1",
|
||||
"schema":"mvp.node.sampler.health.v1",
|
||||
"run_id":context.run_id,
|
||||
"node_id":context.node_id,
|
||||
"stage_index":context.stage_index,
|
||||
|
|
@ -494,7 +494,7 @@ fn sampler_health_payload(
|
|||
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
|
||||
"monotonic_ms":benchmark["monotonic_ms"].clone(),
|
||||
"clock_source":benchmark["clock_source"].clone(),
|
||||
"span_id":format!("myelin-worker:{}:{}:{}:host_sampler_health", context.run_id, context.node_id, benchmark["producer_sequence"]),
|
||||
"span_id":format!("mvp-worker-node:{}:{}:{}:host_sampler_health", context.run_id, context.node_id, benchmark["producer_sequence"]),
|
||||
"parent_span_id":Value::Null,
|
||||
"phase":"host_sampler_health",
|
||||
"status":status,
|
||||
|
|
@ -1013,7 +1013,7 @@ impl WorkerEdgeRuntime {
|
|||
);
|
||||
let step_started = Instant::now();
|
||||
let mut pump = || pump_network(driver, stack);
|
||||
let committed_bytes = match worker.execute_step(
|
||||
let committed_bytes = worker.execute_step(
|
||||
u64::from(config.stage_index) + 1,
|
||||
step_id,
|
||||
object_id,
|
||||
|
|
@ -1027,15 +1027,7 @@ impl WorkerEdgeRuntime {
|
|||
config,
|
||||
datastream,
|
||||
&mut pump,
|
||||
) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
let _ = stack
|
||||
.runtime
|
||||
.send_to(node_actor, NodeAgentMsg::StepFailed { step_id });
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
)?;
|
||||
let helper_execute_ms = duration_ms_u64(step_started.elapsed());
|
||||
let egress_read_started = Instant::now();
|
||||
let record = {
|
||||
|
|
@ -1043,16 +1035,9 @@ impl WorkerEdgeRuntime {
|
|||
let lease = arena
|
||||
.lookup_lease(arena::RingId(output_ring_id))
|
||||
.ok_or_else(|| format!("outbound ring {output_ring_id} lease missing"))?;
|
||||
arena.read_arena(lease.layout.data_offset, committed_bytes)
|
||||
};
|
||||
let record = match record {
|
||||
Ok(record) => record,
|
||||
Err(e) => {
|
||||
let _ = stack
|
||||
.runtime
|
||||
.send_to(node_actor, NodeAgentMsg::OutputFault { edge_id: outbound.edge_id });
|
||||
return Err(format!("read egress ring: {e}"));
|
||||
}
|
||||
arena
|
||||
.read_arena(lease.layout.data_offset, committed_bytes)
|
||||
.map_err(|e| format!("read egress ring: {e}"))?
|
||||
};
|
||||
let egress_read_ms = duration_ms_u64(egress_read_started.elapsed());
|
||||
let record_bytes = record.len();
|
||||
|
|
@ -1082,12 +1067,7 @@ impl WorkerEdgeRuntime {
|
|||
.as_ref()
|
||||
.ok_or_else(|| "outbound edge sender missing".to_owned())?;
|
||||
let edge_send_started = Instant::now();
|
||||
if let Err(e) = sender.send(record) {
|
||||
let _ = stack
|
||||
.runtime
|
||||
.send_to(node_actor, NodeAgentMsg::OutputFault { edge_id: outbound.edge_id });
|
||||
return Err(e);
|
||||
}
|
||||
sender.send(record)?;
|
||||
let edge_send_ms = duration_ms_u64(edge_send_started.elapsed());
|
||||
node_stage(
|
||||
datastream,
|
||||
|
|
@ -1151,17 +1131,8 @@ impl WorkerEdgeRuntime {
|
|||
buffer.extend_from_slice(&bytes);
|
||||
let buffered_bytes = buffer.len();
|
||||
let mut records = Vec::new();
|
||||
loop {
|
||||
match take_complete_ingress_record(buffer, inbound.object_spec) {
|
||||
Ok(Some(record)) => records.push(record),
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
let _ = stack
|
||||
.runtime
|
||||
.send_to(node_actor, NodeAgentMsg::ObjectFailed { edge_id, object_id: None });
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
while let Some(record) = take_complete_ingress_record(buffer, inbound.object_spec)? {
|
||||
records.push(record);
|
||||
}
|
||||
(records, buffered_bytes)
|
||||
};
|
||||
|
|
@ -1200,22 +1171,14 @@ impl WorkerEdgeRuntime {
|
|||
}),
|
||||
);
|
||||
let object_load_started = Instant::now();
|
||||
let loaded = match worker.ring_readable(
|
||||
let loaded = worker.ring_readable(
|
||||
ring_id,
|
||||
edge_id,
|
||||
inbound.object_spec,
|
||||
config,
|
||||
datastream,
|
||||
&mut || {},
|
||||
) {
|
||||
Ok(loaded) => loaded,
|
||||
Err(e) => {
|
||||
let _ = stack
|
||||
.runtime
|
||||
.send_to(node_actor, NodeAgentMsg::ObjectFailed { edge_id, object_id: Some(record.object_id) });
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
)?;
|
||||
let object_load_ms = duration_ms_u64(object_load_started.elapsed());
|
||||
let key = ObjectKey {
|
||||
edge_id,
|
||||
|
|
@ -1547,9 +1510,11 @@ impl WorkerEdgeRuntime {
|
|||
.runtime
|
||||
.send_to(
|
||||
node_actor,
|
||||
NodeAgentMsg::EdgeFault { edge_id: edge_id.0 },
|
||||
NodeAgentMsg::WorkerCrashed {
|
||||
reason: Some(format!("edge {} faulted: {reason:?}", edge_id.0)),
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("report edge fault: {e}"))?;
|
||||
.map_err(|e| format!("mark worker crashed after edge fault: {e}"))?;
|
||||
return Err(format!("edge {} faulted: {reason:?}", edge_id.0));
|
||||
}
|
||||
edge::EdgeLifecycleEvent::EdgeStopped { .. } => {}
|
||||
|
|
@ -1619,7 +1584,7 @@ pub(crate) fn run_from_env() -> ExitCode {
|
|||
_ => match run() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("myelin-worker: {error}");
|
||||
eprintln!("mvp-worker-node: {error}");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
},
|
||||
|
|
@ -1671,7 +1636,7 @@ fn run() -> Result<(), String> {
|
|||
boot(
|
||||
"process",
|
||||
"started",
|
||||
json!({"binary":"myelin-worker","pid":std::process::id()}),
|
||||
json!({"binary":"mvp-worker-node","pid":std::process::id()}),
|
||||
)?;
|
||||
|
||||
let tokio = match tokio::runtime::Runtime::new() {
|
||||
|
|
@ -1722,20 +1687,17 @@ fn run() -> Result<(), String> {
|
|||
boot(
|
||||
"coordinator_join",
|
||||
"skipped",
|
||||
json!({"reason":"MYELIN_COORDINATOR_ENDPOINT not set","mode":"standalone"}),
|
||||
json!({"reason":"MVP_COORDINATOR_ENDPOINT not set","mode":"standalone"}),
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut datastream = NodeDatastream::new(&config);
|
||||
let worker_stats_hook = datastream.producer.stats_hook();
|
||||
let stack = DistributionRuntimeStack::new_with_codecs(
|
||||
driver.node_id(),
|
||||
DistributedNodeConfig::default(),
|
||||
|registry| {
|
||||
register_myelin_actor_codecs(registry);
|
||||
register_mvp_actor_codecs(registry);
|
||||
datastream::wire::register_datastream_codec(registry);
|
||||
},
|
||||
Some(worker_stats_hook),
|
||||
);
|
||||
boot(
|
||||
"distribution_stack",
|
||||
|
|
@ -1788,6 +1750,7 @@ fn run() -> Result<(), String> {
|
|||
};
|
||||
let arena_fd = arena_manager.lock().arena_fd();
|
||||
|
||||
let mut datastream = NodeDatastream::new(&config);
|
||||
let node_boot = |ds: &mut NodeDatastream, phase: &str, status: &str, detail: Value| {
|
||||
emit_node_event(ds, &config, NODE_BOOTSTRAP_CHANNEL, phase, status, detail)
|
||||
};
|
||||
|
|
@ -1843,7 +1806,7 @@ fn run() -> Result<(), String> {
|
|||
json!({"actor":datastream_publisher,"name":DATASTREAM_PUBLISHER_NAME,"subscription_transport":"iroh"}),
|
||||
);
|
||||
let worker_synthetic_id = format!(
|
||||
"myelin-worker-{}-{}-datastream-preflight",
|
||||
"mvp-worker-node-{}-{}-datastream-preflight",
|
||||
config.logical_node_id, config.stage_index
|
||||
);
|
||||
for (phase, status) in [
|
||||
|
|
@ -1857,7 +1820,7 @@ fn run() -> Result<(), String> {
|
|||
phase,
|
||||
status,
|
||||
json!({
|
||||
"producer":"myelin-worker",
|
||||
"producer":"mvp-worker-node",
|
||||
"producer_class":"rust-worker-node",
|
||||
"synthetic_id":worker_synthetic_id,
|
||||
"datastream_endpoint":{
|
||||
|
|
@ -1897,7 +1860,7 @@ fn run() -> Result<(), String> {
|
|||
&mut datastream,
|
||||
"debug_join_socket",
|
||||
"skipped",
|
||||
json!({"reason":"MYELIN_DEBUG_JOIN_SOCKET=disabled"}),
|
||||
json!({"reason":"MVP_DEBUG_JOIN_SOCKET=disabled"}),
|
||||
);
|
||||
None
|
||||
}
|
||||
|
|
@ -1918,7 +1881,7 @@ fn run() -> Result<(), String> {
|
|||
}
|
||||
};
|
||||
let orchestrator = config.orchestrator_actor.ok_or_else(|| {
|
||||
"MYELIN_ORCHESTRATOR_ACTOR is required for runtime readiness signaling".to_owned()
|
||||
"MVP_ORCHESTRATOR_ACTOR is required for runtime readiness signaling".to_owned()
|
||||
})?;
|
||||
let orchestrator_source = "env";
|
||||
boot(
|
||||
|
|
@ -2108,7 +2071,7 @@ fn run() -> Result<(), String> {
|
|||
&mut datastream,
|
||||
"datastream_handoff",
|
||||
"ready",
|
||||
json!({"from":"runtime_ready_ack","to":"cluster_datastream","channel":"myelin.node.ready"}),
|
||||
json!({"from":"runtime_ready_ack","to":"cluster_datastream","channel":"mvp.node.ready"}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2251,7 +2214,7 @@ impl NodeDatastream {
|
|||
let endpoint = Arc::new(DatastreamEndpoint::with_descriptor(
|
||||
StreamDescriptor {
|
||||
stream: stream.clone(),
|
||||
label: Some("myelin worker node".to_owned()),
|
||||
label: Some("mvp worker node".to_owned()),
|
||||
origin: StreamOrigin::RemoteNode,
|
||||
},
|
||||
256,
|
||||
|
|
@ -2269,16 +2232,16 @@ impl NodeDatastream {
|
|||
NODE_PROMPT_CHANNEL,
|
||||
NODE_SHUTDOWN_CHANNEL,
|
||||
NODE_SAMPLER_CHANNEL,
|
||||
"myelin.worker.initialize",
|
||||
"myelin.worker.role",
|
||||
"myelin.worker.weights",
|
||||
"myelin.worker.prompt",
|
||||
"myelin.worker.tokenizer",
|
||||
"myelin.worker.ring",
|
||||
"myelin.worker.ingress",
|
||||
"myelin.worker.step",
|
||||
"myelin.worker.device_object",
|
||||
"myelin.worker.shutdown",
|
||||
"mvp.worker.initialize",
|
||||
"mvp.worker.role",
|
||||
"mvp.worker.weights",
|
||||
"mvp.worker.prompt",
|
||||
"mvp.worker.tokenizer",
|
||||
"mvp.worker.ring",
|
||||
"mvp.worker.ingress",
|
||||
"mvp.worker.step",
|
||||
"mvp.worker.device_object",
|
||||
"mvp.worker.shutdown",
|
||||
] {
|
||||
register_json_channel(&producer, &mut by_name, &mut by_id, name);
|
||||
}
|
||||
|
|
@ -2288,25 +2251,25 @@ impl NodeDatastream {
|
|||
&producer,
|
||||
&mut by_name,
|
||||
&mut by_id,
|
||||
"myelin.node.ready",
|
||||
"mvp.node.ready",
|
||||
),
|
||||
node_lifecycle: register_json_channel(
|
||||
&producer,
|
||||
&mut by_name,
|
||||
&mut by_id,
|
||||
"myelin.node.lifecycle",
|
||||
"mvp.node.lifecycle",
|
||||
),
|
||||
node_self_test: register_json_channel(
|
||||
&producer,
|
||||
&mut by_name,
|
||||
&mut by_id,
|
||||
"myelin.node.self_test",
|
||||
"mvp.node.self_test",
|
||||
),
|
||||
worker_stderr: register_json_channel(
|
||||
&producer,
|
||||
&mut by_name,
|
||||
&mut by_id,
|
||||
"myelin.worker.stderr",
|
||||
"mvp.worker.stderr",
|
||||
),
|
||||
host_cpu: register_record_channel::<datastream::hardware::cpu::HostCpuSample>(
|
||||
&producer,
|
||||
|
|
@ -3255,10 +3218,10 @@ fn publish_stage_shard_fetch_event(
|
|||
config: &DeploymentConfig,
|
||||
event: &Value,
|
||||
) -> Result<(), String> {
|
||||
let channel = datastream.channel_by_name("myelin.worker.weights");
|
||||
let channel = datastream.channel_by_name("mvp.worker.weights");
|
||||
let payload = node_event_payload(config, "stage_shard_fetch", "event", event.clone());
|
||||
datastream.submit_text(channel, payload.to_string());
|
||||
emit_stdio_datastream_frame("myelin.worker.weights", &payload)
|
||||
emit_stdio_datastream_frame("mvp.worker.weights", &payload)
|
||||
.map_err(|e| format!("emit stage shard fetch datastream frame: {e}"))?;
|
||||
datastream.tick();
|
||||
Ok(())
|
||||
|
|
@ -3271,11 +3234,11 @@ fn materialize_stage_shard_with_process(
|
|||
driver: &mut IrohDriver,
|
||||
stack: &DistributionRuntimeStack,
|
||||
) -> Result<PathBuf, String> {
|
||||
let output_path = std::env::var("MYELIN_MODEL_CACHE_DIR")
|
||||
let output_path = std::env::var("MVP_MODEL_CACHE_DIR")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("/var/cache/myelin-models"))
|
||||
.unwrap_or_else(|| PathBuf::from("/var/cache/mvp-models"))
|
||||
.join(plan.cache_file_name());
|
||||
if output_path.is_file() {
|
||||
match validate_stage_shard_cache(&output_path, plan) {
|
||||
|
|
@ -3597,6 +3560,15 @@ fn handle_stage_command(
|
|||
);
|
||||
Ok(())
|
||||
}
|
||||
StageCommandWire::RewireEdge { .. } => {
|
||||
node_stage(
|
||||
datastream,
|
||||
"rewire_edge",
|
||||
"skipped",
|
||||
json!({"reason":"not implemented in mvp-worker-node image path"}),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
StageCommandWire::ReleaseInputHandle { handle_id, .. } => {
|
||||
edge_runtime.release_input_handle(handle_id, worker, config, datastream, driver, stack)
|
||||
}
|
||||
|
|
@ -3736,22 +3708,22 @@ impl DeploymentConfig {
|
|||
}
|
||||
};
|
||||
}
|
||||
let run_id = env_parse!("MYELIN_RUN_ID", 1)?;
|
||||
let logical_node_id = env_parse!("MYELIN_LOGICAL_NODE_ID", 1)?;
|
||||
let run_id = env_parse!("MVP_RUN_ID", 1)?;
|
||||
let logical_node_id = env_parse!("MVP_LOGICAL_NODE_ID", 1)?;
|
||||
let relay = relay_runtime_config_from_env(run_id)?;
|
||||
let debug_join_socket = match env_optional("MYELIN_DEBUG_JOIN_SOCKET").as_deref() {
|
||||
let debug_join_socket = match env_optional("MVP_DEBUG_JOIN_SOCKET").as_deref() {
|
||||
Some("disabled") => None,
|
||||
Some(path) => Some(path.to_owned()),
|
||||
None => Some(
|
||||
std::env::temp_dir()
|
||||
.join(format!(
|
||||
"myelin-node-debug-join-{run_id}-{logical_node_id}.sock"
|
||||
"mvp-node-debug-join-{run_id}-{logical_node_id}.sock"
|
||||
))
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
),
|
||||
};
|
||||
let provider = env_optional("MYELIN_NODE_PROVIDER").unwrap_or_else(|| "process".to_owned());
|
||||
let provider = env_optional("MVP_NODE_PROVIDER").unwrap_or_else(|| "process".to_owned());
|
||||
let default_device = if provider == "process" {
|
||||
"CPU"
|
||||
} else {
|
||||
|
|
@ -3760,20 +3732,20 @@ impl DeploymentConfig {
|
|||
Ok(Self {
|
||||
run_id,
|
||||
logical_node_id,
|
||||
stage_index: env_parse!("MYELIN_STAGE_INDEX", 0)?,
|
||||
coordinator_endpoint: env_optional("MYELIN_COORDINATOR_ENDPOINT")
|
||||
stage_index: env_parse!("MVP_STAGE_INDEX", 0)?,
|
||||
coordinator_endpoint: env_optional("MVP_COORDINATOR_ENDPOINT")
|
||||
.map(|value| {
|
||||
serde_json::from_str::<EndpointAddr>(&value)
|
||||
.map_err(|e| format!("invalid MYELIN_COORDINATOR_ENDPOINT JSON: {e}"))
|
||||
.map_err(|e| format!("invalid MVP_COORDINATOR_ENDPOINT JSON: {e}"))
|
||||
})
|
||||
.transpose()?,
|
||||
orchestrator_actor: env_optional("MYELIN_ORCHESTRATOR_ACTOR")
|
||||
orchestrator_actor: env_optional("MVP_ORCHESTRATOR_ACTOR")
|
||||
.map(|value| {
|
||||
serde_json::from_str::<ActorAddress>(&value)
|
||||
.map_err(|e| format!("invalid MYELIN_ORCHESTRATOR_ACTOR JSON: {e}"))
|
||||
.map_err(|e| format!("invalid MVP_ORCHESTRATOR_ACTOR JSON: {e}"))
|
||||
})
|
||||
.transpose()?,
|
||||
datastream_frame_log: env_optional("MYELIN_DATASTREAM_FRAME_LOG"),
|
||||
datastream_frame_log: env_optional("MVP_DATASTREAM_FRAME_LOG"),
|
||||
debug_join_socket,
|
||||
relay_mode: relay.mode,
|
||||
endpoint_addr_mask: env_optional(MVP_IROH_ENDPOINT_ADDR_MASK_ENV)
|
||||
|
|
@ -3781,29 +3753,29 @@ impl DeploymentConfig {
|
|||
.map(EndpointAddrMask::parse)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
worker_script: env_optional("MYELIN_TINYGRAD_WORKER")
|
||||
worker_script: env_optional("MVP_TINYGRAD_WORKER")
|
||||
.unwrap_or_else(|| DEFAULT_WORKER_SCRIPT.to_owned()),
|
||||
device: env_optional("DEV").unwrap_or_else(|| default_device.to_owned()),
|
||||
model_id: env_optional("MYELIN_MODEL_ID").unwrap_or_else(|| DEFAULT_MODEL_ID.to_owned()),
|
||||
gguf_source: if let Some(path) = env_optional("MYELIN_GGUF_LOCAL_PATH") {
|
||||
model_id: env_optional("MVP_MODEL_ID").unwrap_or_else(|| DEFAULT_MODEL_ID.to_owned()),
|
||||
gguf_source: if let Some(path) = env_optional("MVP_GGUF_LOCAL_PATH") {
|
||||
GgufSource::LocalPath(path)
|
||||
} else {
|
||||
GgufSource::HuggingFaceGguf {
|
||||
repo: env_optional("MYELIN_GGUF_REPO")
|
||||
repo: env_optional("MVP_GGUF_REPO")
|
||||
.unwrap_or_else(|| DEFAULT_HF_REPO.to_owned()),
|
||||
file: env_optional("MYELIN_GGUF_FILE")
|
||||
file: env_optional("MVP_GGUF_FILE")
|
||||
.unwrap_or_else(|| DEFAULT_HF_FILE.to_owned()),
|
||||
revision: env_optional("MYELIN_GGUF_REVISION"),
|
||||
revision: env_optional("MVP_GGUF_REVISION"),
|
||||
}
|
||||
},
|
||||
tokenizer: env_optional("MYELIN_TOKENIZER_LOCAL_PATH")
|
||||
tokenizer: env_optional("MVP_TOKENIZER_LOCAL_PATH")
|
||||
.map(TokenizerSource::LocalPath)
|
||||
.unwrap_or(TokenizerSource::EmbeddedGguf),
|
||||
self_test_prompt: env_optional("MYELIN_NODE_SELF_TEST_PROMPT"),
|
||||
self_test_layer_end: env_parse!("MYELIN_SELF_TEST_LAYER_END", 16)?,
|
||||
self_test_max_tokens: env_parse!("MYELIN_SELF_TEST_MAX_TOKENS", 1)?,
|
||||
arena_bytes: env_parse!("MYELIN_ARENA_BYTES", DEFAULT_ARENA_BYTES)?,
|
||||
arena_alignment: env_parse!("MYELIN_ARENA_ALIGNMENT", DEFAULT_ARENA_ALIGNMENT)?,
|
||||
self_test_prompt: env_optional("MVP_NODE_SELF_TEST_PROMPT"),
|
||||
self_test_layer_end: env_parse!("MVP_SELF_TEST_LAYER_END", 16)?,
|
||||
self_test_max_tokens: env_parse!("MVP_SELF_TEST_MAX_TOKENS", 1)?,
|
||||
arena_bytes: env_parse!("MVP_ARENA_BYTES", DEFAULT_ARENA_BYTES)?,
|
||||
arena_alignment: env_parse!("MVP_ARENA_ALIGNMENT", DEFAULT_ARENA_ALIGNMENT)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -4035,20 +4007,20 @@ impl TinygradWorker {
|
|||
let mut child = Command::new("python3")
|
||||
.arg(&config.worker_script)
|
||||
.env("DEV", &config.device)
|
||||
.env("MYELIN_RUN_ID", config.run_id.to_string())
|
||||
.env("MYELIN_LOGICAL_NODE_ID", config.logical_node_id.to_string())
|
||||
.env("MYELIN_STAGE_INDEX", config.stage_index.to_string())
|
||||
.env("MYELIN_ARENA_FD", arena_fd.to_string())
|
||||
.env("MYELIN_ARENA_BYTES", config.arena_bytes.to_string())
|
||||
.env("MVP_RUN_ID", config.run_id.to_string())
|
||||
.env("MVP_LOGICAL_NODE_ID", config.logical_node_id.to_string())
|
||||
.env("MVP_STAGE_INDEX", config.stage_index.to_string())
|
||||
.env("MVP_ARENA_FD", arena_fd.to_string())
|
||||
.env("MVP_ARENA_BYTES", config.arena_bytes.to_string())
|
||||
.env(
|
||||
"MYELIN_DATASTREAM_ENDPOINT_ID",
|
||||
"MVP_DATASTREAM_ENDPOINT_ID",
|
||||
format!(
|
||||
"worker-node-{}-stage-{}-stdio-bridge",
|
||||
config.logical_node_id, config.stage_index
|
||||
),
|
||||
)
|
||||
.env(
|
||||
"MYELIN_BENCHMARK_PRODUCER_INSTANCE",
|
||||
"MVP_BENCHMARK_PRODUCER_INSTANCE",
|
||||
format!(
|
||||
"tinygrad-worker:{}:{}",
|
||||
config.logical_node_id, config.stage_index
|
||||
|
|
@ -4058,7 +4030,7 @@ impl TinygradWorker {
|
|||
"MVP_IROH_ENDPOINT_ADDR_MASK",
|
||||
config.endpoint_addr_mask.as_str(),
|
||||
)
|
||||
.env("MYELIN_IROH_RELAY_MODE", format!("{:?}", config.relay_mode))
|
||||
.env("MVP_IROH_RELAY_MODE", format!("{:?}", config.relay_mode))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
|
|
@ -4106,7 +4078,7 @@ impl TinygradWorker {
|
|||
"WorkerReady",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.initialize",
|
||||
"mvp.worker.initialize",
|
||||
pump,
|
||||
)
|
||||
.map(|_| ())
|
||||
|
|
@ -4136,7 +4108,7 @@ impl TinygradWorker {
|
|||
"RoleConfigured",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.role",
|
||||
"mvp.worker.role",
|
||||
pump,
|
||||
)
|
||||
.map(|_| ())
|
||||
|
|
@ -4165,7 +4137,7 @@ impl TinygradWorker {
|
|||
"WeightsLoaded",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.weights",
|
||||
"mvp.worker.weights",
|
||||
pump,
|
||||
)
|
||||
.map(|_| ())
|
||||
|
|
@ -4185,7 +4157,7 @@ impl TinygradWorker {
|
|||
"PromptCompleted",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.prompt",
|
||||
"mvp.worker.prompt",
|
||||
pump,
|
||||
)
|
||||
}
|
||||
|
|
@ -4203,7 +4175,7 @@ impl TinygradWorker {
|
|||
"PromptEncoded",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.tokenizer",
|
||||
"mvp.worker.tokenizer",
|
||||
pump,
|
||||
)?;
|
||||
result
|
||||
|
|
@ -4234,7 +4206,7 @@ impl TinygradWorker {
|
|||
"TokensDecoded",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.tokenizer",
|
||||
"mvp.worker.tokenizer",
|
||||
pump,
|
||||
)?;
|
||||
result
|
||||
|
|
@ -4279,7 +4251,7 @@ impl TinygradWorker {
|
|||
"RingInstalled",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.ring",
|
||||
"mvp.worker.ring",
|
||||
pump,
|
||||
)
|
||||
.map(|_| ())
|
||||
|
|
@ -4297,7 +4269,7 @@ impl TinygradWorker {
|
|||
"RingUninstalled",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.ring",
|
||||
"mvp.worker.ring",
|
||||
pump,
|
||||
)
|
||||
.map(|_| ())
|
||||
|
|
@ -4325,7 +4297,7 @@ impl TinygradWorker {
|
|||
"ObjectLoaded",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.ingress",
|
||||
"mvp.worker.ingress",
|
||||
pump,
|
||||
)?;
|
||||
Ok(LoadedObject {
|
||||
|
|
@ -4373,7 +4345,7 @@ impl TinygradWorker {
|
|||
"StepExecuted",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.step",
|
||||
"mvp.worker.step",
|
||||
pump,
|
||||
)?;
|
||||
usize::try_from(value_u64(&event, "committed_bytes")?)
|
||||
|
|
@ -4392,7 +4364,7 @@ impl TinygradWorker {
|
|||
"DeviceObjectReleased",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.device_object",
|
||||
"mvp.worker.device_object",
|
||||
pump,
|
||||
)
|
||||
.map(|_| ())
|
||||
|
|
@ -4409,7 +4381,7 @@ impl TinygradWorker {
|
|||
"WorkerStopped",
|
||||
config,
|
||||
datastream,
|
||||
"myelin.worker.shutdown",
|
||||
"mvp.worker.shutdown",
|
||||
pump,
|
||||
)
|
||||
.map(|_| ())
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! Dashboard view over Myelin cluster provisioning and lifecycle datastream records.
|
||||
//! Dashboard view over MVP cluster provisioning and lifecycle datastream records.
|
||||
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
|
||||
|
|
@ -12,29 +12,29 @@ use std::sync::RwLock;
|
|||
|
||||
use crate::observability::lifecycle as obs;
|
||||
use crate::observability::telemetry::{
|
||||
MYELIN_LIFECYCLE, MYELIN_PROVISIONING_EVENTS, MYELIN_PROVISIONING_LOGS, MyelinLifecycleRecord,
|
||||
MyelinProvisionEventRecord, MyelinProvisionLogRecord,
|
||||
MVP_LIFECYCLE, MVP_PROVISIONING_EVENTS, MVP_PROVISIONING_LOGS, MvpLifecycleRecord,
|
||||
MvpProvisionEventRecord, MvpProvisionLogRecord,
|
||||
};
|
||||
use crate::provisioning::{ProvisionEventKind, ProvisionLogStream};
|
||||
|
||||
const CHANNELS: &[&str] = &[];
|
||||
const PROVISIONING_LOG_PREFIX: &str = "myelin.provisioning.logs.node.";
|
||||
const PROVISIONING_LOG_PREFIX: &str = "mvp.provisioning.logs.node.";
|
||||
const EVENT_LOG_CAP: usize = 256;
|
||||
const LOG_TAIL_CAP: usize = 128;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct MyelinClusterDashboardView {
|
||||
state: RwLock<MyelinClusterDashboardState>,
|
||||
pub(crate) struct MvpClusterDashboardView {
|
||||
state: RwLock<MvpClusterDashboardState>,
|
||||
}
|
||||
|
||||
impl MyelinClusterDashboardView {
|
||||
impl MvpClusterDashboardView {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
struct MyelinClusterDashboardState {
|
||||
struct MvpClusterDashboardState {
|
||||
events: VecDeque<DashboardEventEntry>,
|
||||
nodes: BTreeMap<u64, ProvisionNodeView>,
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ struct ProvisionNodeView {
|
|||
provider_tail: VecDeque<String>,
|
||||
}
|
||||
|
||||
impl MyelinClusterDashboardState {
|
||||
impl MvpClusterDashboardState {
|
||||
fn push_event(&mut self, entry: DashboardEventEntry) {
|
||||
if self.events.len() == EVENT_LOG_CAP {
|
||||
self.events.pop_front();
|
||||
|
|
@ -66,7 +66,7 @@ impl MyelinClusterDashboardState {
|
|||
self.events.push_back(entry);
|
||||
}
|
||||
|
||||
fn apply_provision_event(&mut self, frame: &Frame, record: MyelinProvisionEventRecord) {
|
||||
fn apply_provision_event(&mut self, frame: &Frame, record: MvpProvisionEventRecord) {
|
||||
let phase = provision_kind_label(record.event.kind);
|
||||
let node = self
|
||||
.nodes
|
||||
|
|
@ -78,7 +78,7 @@ impl MyelinClusterDashboardState {
|
|||
node.phase = phase.to_owned();
|
||||
node.last_message = record.event.message.clone();
|
||||
self.push_event(DashboardEventEntry {
|
||||
channel: MYELIN_PROVISIONING_EVENTS.to_owned(),
|
||||
channel: MVP_PROVISIONING_EVENTS.to_owned(),
|
||||
position: frame.position.0,
|
||||
label: phase.to_owned(),
|
||||
run_id: Some(record.event.run_id),
|
||||
|
|
@ -86,7 +86,7 @@ impl MyelinClusterDashboardState {
|
|||
});
|
||||
}
|
||||
|
||||
fn apply_provision_log(&mut self, record: MyelinProvisionLogRecord) {
|
||||
fn apply_provision_log(&mut self, record: MvpProvisionLogRecord) {
|
||||
let node = self
|
||||
.nodes
|
||||
.entry(record.line.node_id)
|
||||
|
|
@ -105,10 +105,10 @@ impl MyelinClusterDashboardState {
|
|||
tail.push_back(record.line.line);
|
||||
}
|
||||
|
||||
fn apply_lifecycle(&mut self, frame: &Frame, record: MyelinLifecycleRecord) {
|
||||
fn apply_lifecycle(&mut self, frame: &Frame, record: MvpLifecycleRecord) {
|
||||
let (run_id, node_id) = lifecycle_ids(&record.event);
|
||||
self.push_event(DashboardEventEntry {
|
||||
channel: MYELIN_LIFECYCLE.to_owned(),
|
||||
channel: MVP_LIFECYCLE.to_owned(),
|
||||
position: frame.position.0,
|
||||
label: format!("{:?}", record.event.kind()),
|
||||
run_id,
|
||||
|
|
@ -117,13 +117,13 @@ impl MyelinClusterDashboardState {
|
|||
}
|
||||
}
|
||||
|
||||
impl DashboardView for MyelinClusterDashboardView {
|
||||
impl DashboardView for MvpClusterDashboardView {
|
||||
fn id(&self) -> &'static str {
|
||||
"myelin/cluster"
|
||||
"mvp/cluster"
|
||||
}
|
||||
|
||||
fn title(&self) -> &'static str {
|
||||
"Myelin cluster"
|
||||
"MVP cluster"
|
||||
}
|
||||
|
||||
fn channels(&self) -> &'static [&'static str] {
|
||||
|
|
@ -131,23 +131,23 @@ impl DashboardView for MyelinClusterDashboardView {
|
|||
}
|
||||
|
||||
fn ingest(&self, _stream: &StreamId, frame: &Frame, event: &FrameEvent) {
|
||||
let mut state = self.state.write().expect("Myelin dashboard state poisoned");
|
||||
let mut state = self.state.write().expect("MVP dashboard state poisoned");
|
||||
match event.channel.as_str() {
|
||||
MYELIN_PROVISIONING_EVENTS => {
|
||||
if let Ok(record) = MyelinProvisionEventRecord::decode(&event.payload) {
|
||||
MVP_PROVISIONING_EVENTS => {
|
||||
if let Ok(record) = MvpProvisionEventRecord::decode(&event.payload) {
|
||||
state.apply_provision_event(frame, record);
|
||||
}
|
||||
}
|
||||
channel
|
||||
if channel == MYELIN_PROVISIONING_LOGS
|
||||
if channel == MVP_PROVISIONING_LOGS
|
||||
|| channel.starts_with(PROVISIONING_LOG_PREFIX) =>
|
||||
{
|
||||
if let Ok(record) = MyelinProvisionLogRecord::decode(&event.payload) {
|
||||
if let Ok(record) = MvpProvisionLogRecord::decode(&event.payload) {
|
||||
state.apply_provision_log(record);
|
||||
}
|
||||
}
|
||||
MYELIN_LIFECYCLE => {
|
||||
if let Ok(record) = MyelinLifecycleRecord::decode(&event.payload) {
|
||||
MVP_LIFECYCLE => {
|
||||
if let Ok(record) = MvpLifecycleRecord::decode(&event.payload) {
|
||||
state.apply_lifecycle(frame, record);
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@ impl DashboardView for MyelinClusterDashboardView {
|
|||
}
|
||||
|
||||
fn snapshot_json(&self) -> Value {
|
||||
let state = self.state.read().expect("Myelin dashboard state poisoned");
|
||||
let state = self.state.read().expect("MVP dashboard state poisoned");
|
||||
json!({
|
||||
"events": state.events,
|
||||
"nodes": state.nodes.values().collect::<Vec<_>>(),
|
||||
|
|
@ -164,7 +164,7 @@ impl DashboardView for MyelinClusterDashboardView {
|
|||
}
|
||||
|
||||
fn html(&self) -> Option<&'static str> {
|
||||
Some(MYELIN_CLUSTER_HTML)
|
||||
Some(MVP_CLUSTER_HTML)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,11 +193,11 @@ fn lifecycle_ids(event: &obs::Event) -> (Option<u64>, Option<u64>) {
|
|||
}
|
||||
}
|
||||
|
||||
const MYELIN_CLUSTER_HTML: &str = r#"<!doctype html>
|
||||
const MVP_CLUSTER_HTML: &str = r#"<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Myelin cluster</title>
|
||||
<title>MVP cluster</title>
|
||||
<style>
|
||||
body { font: 13px system-ui, sans-serif; margin: 1rem; background: #0f1115; color: #e8e8e8; }
|
||||
table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; }
|
||||
|
|
@ -211,7 +211,7 @@ const MYELIN_CLUSTER_HTML: &str = r#"<!doctype html>
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Myelin cluster</h1>
|
||||
<h1>MVP cluster</h1>
|
||||
<p id="status" class="muted">loading…</p>
|
||||
<h2>Provisioned nodes</h2>
|
||||
<table><thead><tr><th>node</th><th>phase</th><th>message</th></tr></thead><tbody id="nodes"></tbody></table>
|
||||
|
|
@ -222,7 +222,7 @@ const MYELIN_CLUSTER_HTML: &str = r#"<!doctype html>
|
|||
<script>
|
||||
let selected = null;
|
||||
async function refresh() {
|
||||
const data = await fetch('/api/view/myelin/cluster').then(r => r.json());
|
||||
const data = await fetch('/api/view/mvp/cluster').then(r => r.json());
|
||||
document.getElementById('status').textContent = `${data.nodes.length} node(s), ${data.events.length} event(s)`;
|
||||
const nodes = document.getElementById('nodes');
|
||||
nodes.innerHTML = data.nodes.map(n => `<tr data-node="${n.node_id}"><td>${n.node_id}</td><td class="${n.phase === 'provision_failed' ? 'bad' : 'ok'}">${n.phase || ''}</td><td>${n.last_message || ''}</td></tr>`).join('') || '<tr><td colspan="3" class="muted">No provisioning records yet.</td></tr>';
|
||||
|
|
@ -7,9 +7,9 @@ use serde_json::json;
|
|||
|
||||
use crate::observability::benchmark;
|
||||
|
||||
/// JSONL archive for Myelin-owned datastream frames.
|
||||
/// JSONL archive for MVP-owned datastream frames.
|
||||
///
|
||||
/// The datastream crate owns frame transport; this helper owns the Myelin archive
|
||||
/// The datastream crate owns frame transport; this helper owns the MVP archive
|
||||
/// record shape used as benchmark and contract evidence.
|
||||
pub(crate) struct FrameArchive {
|
||||
file: File,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! Myelin observability public surface.
|
||||
//! MVP observability public surface.
|
||||
|
||||
pub(crate) mod benchmark;
|
||||
#[cfg(feature = "dashboard")]
|
||||
|
|
@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_json::Value;
|
||||
|
||||
use crate::observability::telemetry::{
|
||||
MYELIN_PROVISIONING_LOGS, MyelinProvisionLogRecord, myelin_provision_log_channel,
|
||||
MVP_PROVISIONING_LOGS, MvpProvisionLogRecord, mvp_provision_log_channel,
|
||||
};
|
||||
use crate::provisioning::{
|
||||
NodeProvisionSpec, PluginObservation, PluginSink, ProvisionLogLine, ProvisionLogStream,
|
||||
|
|
@ -135,16 +135,16 @@ impl BootstrapDatastreamBridge {
|
|||
let Some(producer) = &self.producer else {
|
||||
return;
|
||||
};
|
||||
let record = MyelinProvisionLogRecord::new(ProvisionLogLine {
|
||||
let record = MvpProvisionLogRecord::new(ProvisionLogLine {
|
||||
run_id: self.spec.run_id,
|
||||
node_id: self.spec.node_id,
|
||||
stream,
|
||||
line: line.to_owned(),
|
||||
});
|
||||
let channel = producer.register_channel(
|
||||
myelin_provision_log_channel(self.spec.node_id, stream),
|
||||
mvp_provision_log_channel(self.spec.node_id, stream),
|
||||
ChannelContent::JsonRecord {
|
||||
schema: Some(MYELIN_PROVISIONING_LOGS.to_owned()),
|
||||
schema: Some(MVP_PROVISIONING_LOGS.to_owned()),
|
||||
},
|
||||
);
|
||||
let payload = serde_json::to_vec(&record).expect("serialize bootstrap log record");
|
||||
|
|
@ -154,7 +154,7 @@ impl BootstrapDatastreamBridge {
|
|||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct StdioDatastreamFrame {
|
||||
myelin_stdio_event: u32,
|
||||
mvp_stdio_event: u32,
|
||||
kind: String,
|
||||
channel: String,
|
||||
payload: Value,
|
||||
|
|
@ -165,7 +165,7 @@ pub(crate) fn parse_stdio_datastream_frame(
|
|||
line: &str,
|
||||
) -> Option<PluginObservation> {
|
||||
let frame = serde_json::from_str::<StdioDatastreamFrame>(line).ok()?;
|
||||
if frame.myelin_stdio_event != 1 || frame.kind != "datastream_frame" {
|
||||
if frame.mvp_stdio_event != 1 || frame.kind != "datastream_frame" {
|
||||
return None;
|
||||
}
|
||||
Some(PluginObservation::DatastreamFrame {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
//! Myelin-system-owned datastream channel records.
|
||||
//! MVP-system-owned datastream channel records.
|
||||
|
||||
use datastream::Record;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -7,61 +7,61 @@ use serde::{Deserialize, Serialize};
|
|||
use crate::observability::lifecycle as obs;
|
||||
use crate::provisioning::{self, ProvisionLogStream};
|
||||
|
||||
/// Structured Myelin lifecycle facts: run, node, stage, edge, ring, object, step, and worker events.
|
||||
pub(crate) const MYELIN_LIFECYCLE: &str = "myelin.lifecycle";
|
||||
/// Structured MVP lifecycle facts: run, node, stage, edge, ring, object, step, and worker events.
|
||||
pub(crate) const MVP_LIFECYCLE: &str = "mvp.lifecycle";
|
||||
/// Structured node provisioning milestones emitted before a remote swactor runtime is live.
|
||||
pub(crate) const MYELIN_PROVISIONING_EVENTS: &str = "myelin.provisioning.events";
|
||||
pub(crate) const MVP_PROVISIONING_EVENTS: &str = "mvp.provisioning.events";
|
||||
|
||||
/// Raw provider/process stream lines captured during provisioning.
|
||||
pub(crate) const MYELIN_PROVISIONING_LOGS: &str = "myelin.provisioning.logs";
|
||||
pub(crate) const MVP_PROVISIONING_LOGS: &str = "mvp.provisioning.logs";
|
||||
|
||||
/// Datastream payload for the Myelin lifecycle channel.
|
||||
/// Datastream payload for the MVP lifecycle channel.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct MyelinLifecycleRecord {
|
||||
pub(crate) struct MvpLifecycleRecord {
|
||||
pub event: obs::Event,
|
||||
}
|
||||
|
||||
impl Record for MyelinLifecycleRecord {
|
||||
const CHANNEL: &'static str = MYELIN_LIFECYCLE;
|
||||
impl Record for MvpLifecycleRecord {
|
||||
const CHANNEL: &'static str = MVP_LIFECYCLE;
|
||||
}
|
||||
/// Datastream payload for provisioning lifecycle events.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct MyelinProvisionEventRecord {
|
||||
pub(crate) struct MvpProvisionEventRecord {
|
||||
pub event: provisioning::ProvisionEvent,
|
||||
}
|
||||
|
||||
impl MyelinProvisionEventRecord {
|
||||
impl MvpProvisionEventRecord {
|
||||
pub(crate) fn new(event: provisioning::ProvisionEvent) -> Self {
|
||||
Self { event }
|
||||
}
|
||||
}
|
||||
|
||||
impl Record for MyelinProvisionEventRecord {
|
||||
const CHANNEL: &'static str = MYELIN_PROVISIONING_EVENTS;
|
||||
impl Record for MvpProvisionEventRecord {
|
||||
const CHANNEL: &'static str = MVP_PROVISIONING_EVENTS;
|
||||
}
|
||||
|
||||
/// Datastream payload for provisioning stdout/stderr/provider lines.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct MyelinProvisionLogRecord {
|
||||
pub(crate) struct MvpProvisionLogRecord {
|
||||
pub line: provisioning::ProvisionLogLine,
|
||||
}
|
||||
|
||||
impl MyelinProvisionLogRecord {
|
||||
impl MvpProvisionLogRecord {
|
||||
pub(crate) fn new(line: provisioning::ProvisionLogLine) -> Self {
|
||||
Self { line }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn myelin_provision_log_channel(node_id: u64, stream: ProvisionLogStream) -> String {
|
||||
pub(crate) fn mvp_provision_log_channel(node_id: u64, stream: ProvisionLogStream) -> String {
|
||||
let stream = match stream {
|
||||
ProvisionLogStream::Stdout => "stdout",
|
||||
ProvisionLogStream::Stderr => "stderr",
|
||||
ProvisionLogStream::Provider => "provider",
|
||||
};
|
||||
format!("myelin.provisioning.logs.node.{node_id}.{stream}")
|
||||
format!("mvp.provisioning.logs.node.{node_id}.{stream}")
|
||||
}
|
||||
|
||||
impl Record for MyelinProvisionLogRecord {
|
||||
const CHANNEL: &'static str = MYELIN_PROVISIONING_LOGS;
|
||||
impl Record for MvpProvisionLogRecord {
|
||||
const CHANNEL: &'static str = MVP_PROVISIONING_LOGS;
|
||||
}
|
||||
|
||||
|
|
@ -68,13 +68,6 @@ pub(crate) enum OrchestratorMsg {
|
|||
stage_index: u32,
|
||||
},
|
||||
ObserveTokenEndpointsStopped,
|
||||
ObserveOperatorStop {
|
||||
run_id: u64,
|
||||
},
|
||||
ObserveMembershipLost {
|
||||
run_id: u64,
|
||||
node_id: u64,
|
||||
},
|
||||
AdvanceTimeMs(u64),
|
||||
Snapshot {
|
||||
reply_to: ActorAddress,
|
||||
|
|
@ -83,7 +76,7 @@ pub(crate) enum OrchestratorMsg {
|
|||
|
||||
impl NetworkMessage for OrchestratorMsg {
|
||||
fn type_tag() -> &'static str {
|
||||
"myelin::OrchestratorMsg"
|
||||
"mvp_system::OrchestratorMsg"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +131,7 @@ pub(crate) enum RunCommandWire {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) enum LifecycleEventWire {
|
||||
RunRejected { run_id: u64 },
|
||||
RunFaulted { run_id: u64 },
|
||||
RunCompleted { run_id: u64 },
|
||||
RunOperatorStopped { run_id: u64 },
|
||||
|
|
@ -186,7 +180,7 @@ pub(crate) enum OrchestratorReport {
|
|||
|
||||
impl NetworkMessage for OrchestratorReport {
|
||||
fn type_tag() -> &'static str {
|
||||
"myelin::OrchestratorReport"
|
||||
"mvp_system::OrchestratorReport"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,17 +272,6 @@ impl OrchestratorActor {
|
|||
OrchestratorMsg::ObserveTokenEndpointsStopped => {
|
||||
self.core.observe(core::RunEvent::TokenEndpointsStopped)
|
||||
}
|
||||
OrchestratorMsg::ObserveOperatorStop { run_id } => {
|
||||
self.core.observe(core::RunEvent::OperatorStop {
|
||||
run_id: core::RunId(run_id),
|
||||
});
|
||||
}
|
||||
OrchestratorMsg::ObserveMembershipLost { run_id, node_id } => {
|
||||
self.core.observe(core::RunEvent::MembershipLost {
|
||||
run_id: core::RunId(run_id),
|
||||
node_id: core::NodeId(node_id),
|
||||
});
|
||||
}
|
||||
OrchestratorMsg::AdvanceTimeMs(delta) => self.core.advance_time_ms(delta),
|
||||
}
|
||||
}
|
||||
|
|
@ -466,6 +449,9 @@ impl From<&core::RunCommand> for RunCommandWire {
|
|||
impl From<&core::LifecycleEvent> for LifecycleEventWire {
|
||||
fn from(event: &core::LifecycleEvent) -> Self {
|
||||
match event {
|
||||
core::LifecycleEvent::RunRejected { run_id, .. } => {
|
||||
Self::RunRejected { run_id: run_id.0 }
|
||||
}
|
||||
core::LifecycleEvent::RunFaulted { run_id, .. } => {
|
||||
Self::RunFaulted { run_id: run_id.0 }
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -88,7 +88,7 @@ pub(crate) fn looks_remote_image(image: &str) -> bool {
|
|||
}
|
||||
|
||||
/// Shared overlay for legacy and multi-binary configuration parsing. This accepts
|
||||
/// fields outside the fixed `myelin-chat` public config surface; `myelin-chat` uses a
|
||||
/// fields outside the fixed `mvp-chat` public config surface; `mvp-chat` uses a
|
||||
/// bin-local strict config loader instead.
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
//! Myelin-system swactor distribution runtime wiring.
|
||||
//! MVP-system swactor distribution runtime wiring.
|
||||
//!
|
||||
//! This is the production version of the actor-stack setup that integration
|
||||
//! tests used to copy by hand: a swactor runtime, the four distribution protocol
|
||||
|
|
@ -13,7 +13,6 @@ use std::time::{Duration, Instant};
|
|||
use swactor::actor::{ActorAddress, ActorInterface};
|
||||
use swactor::config::RuntimeConfig;
|
||||
use swactor::runtime::{Ctx, Runtime};
|
||||
use swactor::stats::StatsHook;
|
||||
use swactor::std::StdExtension;
|
||||
use swactor_transport::{CodecRegistry, CodecRemoteSink, NetworkMessage, TransportRouter};
|
||||
|
||||
|
|
@ -45,6 +44,7 @@ pub(crate) struct DistributionActorAddrs {
|
|||
}
|
||||
|
||||
pub(crate) struct DistributionRuntimeStack {
|
||||
pub node_id: NodeId,
|
||||
pub runtime: Arc<Runtime>,
|
||||
pub codec: Arc<CodecRegistry>,
|
||||
pub outbox: Outbox,
|
||||
|
|
@ -61,7 +61,6 @@ impl DistributionRuntimeStack {
|
|||
node_id: NodeId,
|
||||
config: DistributedNodeConfig,
|
||||
extend_codecs: impl FnOnce(&mut CodecRegistry),
|
||||
stats_hook: Option<Arc<dyn StatsHook>>,
|
||||
) -> Self {
|
||||
let mut runtime =
|
||||
Runtime::new(RuntimeConfig::default()).with_extension(Arc::new(StdExtension::new()));
|
||||
|
|
@ -73,9 +72,6 @@ impl DistributionRuntimeStack {
|
|||
Arc::clone(&codec),
|
||||
Arc::clone(&transport_router),
|
||||
)));
|
||||
if let Some(hook) = stats_hook {
|
||||
runtime.set_stats_hook(hook);
|
||||
}
|
||||
let runtime = Arc::new(runtime);
|
||||
|
||||
let outbox: Outbox = Arc::new(Mutex::new(Vec::new()));
|
||||
|
|
@ -150,6 +146,7 @@ impl DistributionRuntimeStack {
|
|||
.expect("subscribe MembershipFanout");
|
||||
|
||||
Self {
|
||||
node_id,
|
||||
runtime,
|
||||
codec,
|
||||
outbox,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! Myelin run orchestration public surface.
|
||||
//! MVP run orchestration public surface.
|
||||
//!
|
||||
//! This module owns run planning, membership/readiness coordination, deployment
|
||||
//! dispatch, run authority, prompt admission, and teardown behavior. Provider
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//! Myelin-facing compatibility surface for reusable provider-neutral node provisioning.
|
||||
//! MVP-facing compatibility surface for reusable provider-neutral node provisioning.
|
||||
//!
|
||||
//! The reusable crate keeps `ProviderKind` opaque. Myelin provider selection policy
|
||||
//! The reusable crate keeps `ProviderKind` opaque. MVP provider selection policy
|
||||
//! lives here so the reusable contracts do not know about process, Docker, or
|
||||
//! VastAI runtime choices.
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
//! Relay provisioning shims for Myelin runtimes.
|
||||
//! Relay provisioning shims for MVP runtimes.
|
||||
//!
|
||||
//! The current implementations are deliberately small: local tests get the same
|
||||
//! abstraction without an external relay, and deploy runs can point every node at
|
||||
|
|
@ -9,8 +9,8 @@
|
|||
use iroh::{RelayMode, RelayUrl};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub(crate) const MYELIN_IROH_RELAY_MODE_ENV: &str = "MYELIN_IROH_RELAY_MODE";
|
||||
pub(crate) const MYELIN_IROH_RELAY_URL_ENV: &str = "MYELIN_IROH_RELAY_URL";
|
||||
pub(crate) const MVP_IROH_RELAY_MODE_ENV: &str = "MVP_IROH_RELAY_MODE";
|
||||
pub(crate) const MVP_IROH_RELAY_URL_ENV: &str = "MVP_IROH_RELAY_URL";
|
||||
pub(crate) const SWACTOR_IROH_RELAY_URL_ENV: &str = "SWACTOR_IROH_RELAY_URL";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -72,6 +72,12 @@ impl StaticRelayProvider {
|
|||
parse_relay_url(raw).map(Self::new)
|
||||
}
|
||||
|
||||
pub(crate) fn from_env() -> Result<Option<Self>, String> {
|
||||
selected_relay_url_from_env()
|
||||
.map(|url| Self::from_url_str(&url).map(Some))
|
||||
.unwrap_or(Ok(None))
|
||||
}
|
||||
|
||||
pub(crate) fn url(&self) -> String {
|
||||
self.url.to_string()
|
||||
}
|
||||
|
|
@ -102,7 +108,7 @@ impl RelayProvider for StaticRelayProvider {
|
|||
}
|
||||
|
||||
pub(crate) fn relay_runtime_config_from_env(run_id: u64) -> Result<RelayRuntimeConfig, String> {
|
||||
let mode = env_optional(MYELIN_IROH_RELAY_MODE_ENV).map(|value| value.to_ascii_lowercase());
|
||||
let mode = env_optional(MVP_IROH_RELAY_MODE_ENV).map(|value| value.to_ascii_lowercase());
|
||||
let url = selected_relay_url_from_env();
|
||||
relay_runtime_config_from_settings(run_id, mode.as_deref(), url.as_deref())
|
||||
}
|
||||
|
|
@ -119,7 +125,7 @@ pub(crate) fn relay_runtime_config_from_settings(
|
|||
}),
|
||||
None | Some("default") => relay_runtime_config_from_optional_static_provider(run_id, url),
|
||||
Some(other) => Err(format!(
|
||||
"unsupported {MYELIN_IROH_RELAY_MODE_ENV}={other:?}; use disabled or default"
|
||||
"unsupported {MVP_IROH_RELAY_MODE_ENV}={other:?}; use disabled or default"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -132,7 +138,7 @@ pub(crate) fn relay_mode_env_value(mode: &RelayMode) -> &'static str {
|
|||
}
|
||||
|
||||
pub(crate) fn selected_relay_url_from_env() -> Option<String> {
|
||||
env_optional(MYELIN_IROH_RELAY_URL_ENV).or_else(|| env_optional(SWACTOR_IROH_RELAY_URL_ENV))
|
||||
env_optional(MVP_IROH_RELAY_URL_ENV).or_else(|| env_optional(SWACTOR_IROH_RELAY_URL_ENV))
|
||||
}
|
||||
|
||||
fn relay_runtime_config_from_optional_static_provider(
|
||||
|
|
@ -4,13 +4,12 @@ use std::io::{BufRead, BufReader, Read};
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::{Arc, mpsc};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use datastream::DatastreamProducer;
|
||||
use swactor::actor::{ActorAddress, ActorInterface};
|
||||
use swactor::runtime::{Ctx, ExternalSender, Runtime, RuntimeConfig};
|
||||
use swactor::runtime::{Ctx, ExternalSender, Runtime, RuntimeConfig, RuntimeHandle};
|
||||
use swactor_vastai::{
|
||||
CreateInstanceRequest, LifecyclePolicy, Offer, ProvisionRequest, ProvisionedInstance,
|
||||
SelectionPolicy, classify_vastai_error, create_instance,
|
||||
|
|
@ -36,7 +35,7 @@ pub(crate) struct VastAiProvisioningConfig {
|
|||
impl Default for VastAiProvisioningConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
label_prefix: "myelin".to_owned(),
|
||||
label_prefix: "mvp".to_owned(),
|
||||
disk_gb: 50,
|
||||
ssh_user: "root".to_owned(),
|
||||
selection: SelectionPolicy::default(),
|
||||
|
|
@ -56,46 +55,27 @@ pub(crate) struct VastAiSshEndpoint {
|
|||
}
|
||||
|
||||
pub(crate) struct VastAiProviderMonitor {
|
||||
runtime: Arc<Runtime>,
|
||||
runtime: Option<RuntimeHandle>,
|
||||
actor: ActorAddress,
|
||||
tick_thread: Option<JoinHandle<()>>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl VastAiProviderMonitor {
|
||||
fn new(runtime: Runtime, actor: ActorAddress) -> Self {
|
||||
let runtime = Arc::new(runtime);
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let rt = Arc::clone(&runtime);
|
||||
let flag = Arc::clone(&stop_flag);
|
||||
let tick_thread = thread::spawn(move || {
|
||||
while !flag.load(Ordering::Relaxed) || rt.has_work() {
|
||||
if rt.has_work() {
|
||||
rt.tick();
|
||||
} else {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fn new(runtime: RuntimeHandle, actor: ActorAddress) -> Self {
|
||||
Self {
|
||||
runtime,
|
||||
runtime: Some(runtime),
|
||||
actor,
|
||||
tick_thread: Some(tick_thread),
|
||||
stop_flag,
|
||||
}
|
||||
}
|
||||
|
||||
fn stop(&mut self) {
|
||||
let Some(tick_thread) = self.tick_thread.take() else {
|
||||
let Some(runtime) = self.runtime.take() else {
|
||||
return;
|
||||
};
|
||||
let _ = self
|
||||
let _ = runtime
|
||||
.runtime
|
||||
.send_to(self.actor, VastAiProviderMonitorMsg::Stop);
|
||||
self.stop_flag.store(true, Ordering::Relaxed);
|
||||
let _ = tick_thread.join();
|
||||
runtime.shutdown();
|
||||
runtime.join();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -572,6 +552,7 @@ impl VastAiLeaseClient for ToolsVastAiLeaseClient {
|
|||
sender,
|
||||
))
|
||||
.ok()?;
|
||||
let runtime = runtime.run().ok()?;
|
||||
Some(VastAiProviderMonitor::new(runtime, actor))
|
||||
}
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
//! Myelin-local provisioning provider plugins.
|
||||
//! MVP-local provisioning provider plugins.
|
||||
//!
|
||||
//! Provider-neutral lifecycle contracts live in the reusable `provisioning`
|
||||
//! crate. This module keeps Myelin-local process/Docker plugin implementations
|
||||
//! crate. This module keeps MVP-local process/Docker plugin implementations
|
||||
//! that know about bootstrap datastream plumbing and local runtime execution.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
|
@ -118,7 +118,7 @@ fn prepare_docker_file_volume(
|
|||
&["volume", "create", &volume],
|
||||
"create cached model docker volume",
|
||||
)?;
|
||||
let loader_name = format!("myelin-cache-load-{}-{volume}", std::process::id());
|
||||
let loader_name = format!("mvp-cache-load-{}-{volume}", std::process::id());
|
||||
let _ = Command::new("docker")
|
||||
.args(["rm", "-f", &loader_name])
|
||||
.stdout(Stdio::null())
|
||||
|
|
@ -173,7 +173,7 @@ fn docker_file_cache_volume_name(path: &Path) -> Result<String, String> {
|
|||
.and_then(|name| name.to_str())
|
||||
.map(safe_docker_volume_component)
|
||||
.unwrap_or_else(|| "model".to_owned());
|
||||
Ok(format!("myelin-cache-{file}-{}-{modified}", metadata.len()))
|
||||
Ok(format!("mvp-cache-{file}-{}-{modified}", metadata.len()))
|
||||
}
|
||||
|
||||
fn safe_docker_volume_component(value: &str) -> String {
|
||||
|
|
@ -455,14 +455,14 @@ impl ProvisionPlugin for LocalDockerPlugin {
|
|||
let docker_gpus = spec
|
||||
.env
|
||||
.iter()
|
||||
.find(|(key, _)| key == "MYELIN_DOCKER_GPUS")
|
||||
.find(|(key, _)| key == "MVP_DOCKER_GPUS")
|
||||
.map(|(_, value)| value.clone())
|
||||
.or_else(|| std::env::var("MYELIN_DOCKER_GPUS").ok())
|
||||
.or_else(|| std::env::var("MVP_DOCKER_GPUS").ok())
|
||||
.filter(|value| !value.trim().is_empty());
|
||||
sink.observe(PluginObservation::DatastreamFrame {
|
||||
run_id: spec.run_id,
|
||||
node_id: spec.node_id,
|
||||
channel: "myelin.provisioning.events".to_owned(),
|
||||
channel: "mvp.node.bootstrap".to_owned(),
|
||||
payload: serde_json::json!({
|
||||
"type":"DockerGpuConfigResolved",
|
||||
"provider":"Docker",
|
||||
|
|
@ -16,6 +16,16 @@ pub(crate) struct RunPlan {
|
|||
pub stages: Vec<StageRef>,
|
||||
}
|
||||
|
||||
impl RunPlan {
|
||||
pub(crate) fn test_linear(run_id: RunId, stages: Vec<StageRef>) -> Self {
|
||||
Self { run_id, stages }
|
||||
}
|
||||
|
||||
pub(crate) fn stage_nodes(&self) -> Vec<NodeId> {
|
||||
self.stages.iter().map(|stage| stage.node_id).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct RunConfig {
|
||||
pub run_id: RunId,
|
||||
|
|
@ -115,6 +125,10 @@ pub(crate) enum RunFaultReason {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum LifecycleEvent {
|
||||
RunRejected {
|
||||
run_id: RunId,
|
||||
reason: RunFaultReason,
|
||||
},
|
||||
RunFaulted {
|
||||
run_id: RunId,
|
||||
reason: RunFaultReason,
|
||||
|
|
@ -161,6 +175,8 @@ pub(crate) enum RunCommand {
|
|||
},
|
||||
}
|
||||
|
||||
pub type OrchestratorHarness = OrchestratorRun;
|
||||
|
||||
pub(crate) struct OrchestratorRun {
|
||||
config: RunConfig,
|
||||
plan: Option<RunPlan>,
|
||||
|
|
@ -165,6 +165,7 @@ pub(crate) enum EdgeKind {
|
|||
pub(crate) enum ObjectKind {
|
||||
Token,
|
||||
Activation,
|
||||
Weight,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
3
crates/mvp-system/src/prompt/mod.rs
Normal file
3
crates/mvp-system/src/prompt/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
//! MVP prompt protocol public surface.
|
||||
|
||||
pub(crate) mod rpc;
|
||||
|
|
@ -56,7 +56,7 @@ impl PromptEvent {
|
|||
|
||||
impl NetworkMessage for PromptEvent {
|
||||
fn type_tag() -> &'static str {
|
||||
"myelin::PromptEvent"
|
||||
"mvp_system::PromptEvent"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ pub(crate) enum TokenizerEvent {
|
|||
|
||||
impl NetworkMessage for TokenizerEvent {
|
||||
fn type_tag() -> &'static str {
|
||||
"myelin::TokenizerEvent"
|
||||
"mvp_system::TokenizerEvent"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -19,6 +19,12 @@ pub(crate) struct DeviceHandle {
|
|||
pub id: u64,
|
||||
}
|
||||
|
||||
impl DeviceHandle {
|
||||
pub(crate) fn new_current(id: u64) -> Self {
|
||||
Self { generation: 1, id }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct LayerRange {
|
||||
pub start: u32,
|
||||
|
|
@ -44,6 +50,14 @@ impl WeightSource {
|
|||
tokenizer,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn embedded_gguf(model_id: impl Into<String>, path: impl Into<String>) -> Self {
|
||||
Self::new(
|
||||
model_id,
|
||||
GgufSource::LocalPath(path.into()),
|
||||
TokenizerSource::EmbeddedGguf,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
|
@ -214,6 +228,9 @@ pub(crate) enum StageCommand {
|
|||
range: LayerRange,
|
||||
shard_plan: Option<StageShardPlan>,
|
||||
},
|
||||
RewireEdge {
|
||||
edge_id: EdgeId,
|
||||
},
|
||||
ExecuteStep(ExecuteStep),
|
||||
ReleaseInputHandle {
|
||||
object_id: ObjectId,
|
||||
|
|
@ -227,6 +244,8 @@ pub(crate) enum StageCommand {
|
|||
},
|
||||
}
|
||||
|
||||
pub type StageControllerHarness = StageController;
|
||||
|
||||
pub(crate) struct StageController {
|
||||
local_node_id: NodeId,
|
||||
provision: Option<ProvisionStage>,
|
||||
|
|
@ -35,7 +35,7 @@ impl GgufPlanningMetadata {
|
|||
.map(u64::from)
|
||||
.unwrap_or(DEFAULT_EFFECTIVE_CONTEXT);
|
||||
if requested_context == 0 {
|
||||
return Err("MYELIN_MAX_CONTEXT/--max-context must be greater than 0".to_owned());
|
||||
return Err("MVP_MAX_CONTEXT/--max-context must be greater than 0".to_owned());
|
||||
}
|
||||
let effective_context = requested_context.min(self.context_length);
|
||||
if effective_context == 0 {
|
||||
|
|
@ -728,7 +728,7 @@ fn fetch_http_range(url: &str, start: u64, len: u64) -> Result<Vec<u8>, String>
|
|||
let range = format!("bytes={start}-{end}");
|
||||
let mut request = ureq::get(url)
|
||||
.set("Range", &range)
|
||||
.set("User-Agent", "myelin-node/0.1");
|
||||
.set("User-Agent", "swactor-mvp-node/0.1");
|
||||
if let Ok(token) = std::env::var("HF_TOKEN")
|
||||
&& !token.trim().is_empty()
|
||||
{
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
//! Myelin stage control, shard planning, and weight lifecycle public surface.
|
||||
//! MVP stage control, shard planning, and weight lifecycle public surface.
|
||||
|
||||
pub(crate) mod control;
|
||||
pub(crate) mod gguf_metadata;
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
//! Local end-to-end behavior guarantee for the Myelin system lifecycle.
|
||||
//! Local end-to-end behavior guarantee for the MVP system lifecycle.
|
||||
//!
|
||||
//! This test composes the crate's Myelin contract harnesses through one in-process
|
||||
//! This test composes the crate's MVP contract harnesses through one in-process
|
||||
//! mock environment. It deliberately avoids Docker, real SWIM, real iroh, GGUF,
|
||||
//! and CUDA while still driving the run through planning, provisioning,
|
||||
//! readiness, prompt injection, stage execution, completion, and teardown.
|
||||
|
||||
use myelin::observability::lifecycle as obs;
|
||||
use myelin::orchestration::engine_builder as engine;
|
||||
use mvp_system::observability::lifecycle as obs;
|
||||
use mvp_system::orchestration::engine_builder as engine;
|
||||
|
||||
use super::local_mock::{
|
||||
LocalMockCluster, LocalMockConfig, LocalMockOutcome, assert_happy_path_lifecycle,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use myelin::observability::lifecycle as obs;
|
||||
use mvp_system::observability::lifecycle as obs;
|
||||
|
||||
use super::mock_transport::MockObjectKind;
|
||||
|
||||
|
|
@ -2,11 +2,10 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||
|
||||
use crate::run_fsm as fsm;
|
||||
use crate::run_plan as plan;
|
||||
use crate::tests::harness::OrchestratorHarness;
|
||||
use data_plane::edge_actor;
|
||||
use myelin::observability::lifecycle as obs;
|
||||
use myelin::orchestration::engine_builder as engine;
|
||||
use myelin::staging as stage;
|
||||
use mvp_system::observability::lifecycle as obs;
|
||||
use mvp_system::orchestration::engine_builder as engine;
|
||||
use mvp_system::staging as stage;
|
||||
|
||||
use super::mock_node::MockNode;
|
||||
use super::mock_transport::{Delivery, MockObject, MockObjectKind, MockTransport};
|
||||
|
|
@ -35,7 +34,7 @@ pub struct LocalMockCluster {
|
|||
engine_events: Vec<engine::EngineEvent>,
|
||||
plan: plan::RunPlan,
|
||||
nodes: BTreeMap<u32, MockNode>,
|
||||
orchestrator: Option<OrchestratorHarness>,
|
||||
orchestrator: Option<fsm::OrchestratorHarness>,
|
||||
orchestrator_command_cursor: usize,
|
||||
orchestrator_event_cursor: usize,
|
||||
trace: Vec<obs::Event>,
|
||||
|
|
@ -304,7 +303,7 @@ impl LocalMockCluster {
|
|||
self.orchestrator_command_cursor = 0;
|
||||
self.orchestrator_event_cursor = 0;
|
||||
self.scenario = scenario;
|
||||
self.orchestrator = Some(OrchestratorHarness::new(fsm::RunConfig {
|
||||
self.orchestrator = Some(fsm::OrchestratorHarness::new(fsm::RunConfig {
|
||||
run_id: fsm::RunId(self.run_id.0),
|
||||
max_tokens: u64::from(self.max_tokens),
|
||||
prompt: tokenize(prompt),
|
||||
|
|
@ -743,6 +742,7 @@ impl LocalMockCluster {
|
|||
self.push_run(obs::EventKind::RunCompleted, obs::Component::Orchestrator);
|
||||
}
|
||||
fsm::LifecycleEvent::RunFaulted { .. }
|
||||
| fsm::LifecycleEvent::RunRejected { .. }
|
||||
| fsm::LifecycleEvent::RunOperatorStopped { .. } => {
|
||||
self.push_run(obs::EventKind::RunFaulted, obs::Component::Orchestrator);
|
||||
}
|
||||
|
|
@ -852,7 +852,7 @@ impl LocalMockCluster {
|
|||
.count()
|
||||
}
|
||||
|
||||
fn orchestrator_mut(&mut self) -> &mut OrchestratorHarness {
|
||||
fn orchestrator_mut(&mut self) -> &mut fsm::OrchestratorHarness {
|
||||
self.orchestrator
|
||||
.as_mut()
|
||||
.expect("run_prompt must initialize orchestrator")
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
use crate::run_plan as plan;
|
||||
use data_plane::edge_actor;
|
||||
use myelin::staging as stage;
|
||||
use crate::tests::harness::StageControllerHarness;
|
||||
use mvp_system::staging as stage;
|
||||
|
||||
use super::mock_transport::MockObject;
|
||||
use super::mock_worker::MockWorker;
|
||||
|
|
@ -17,7 +16,7 @@ pub struct MockNode {
|
|||
inbound_edge: Option<plan::EdgeId>,
|
||||
outbound_edge: Option<plan::EdgeId>,
|
||||
outbound_object_allocator: Option<edge_actor::ObjectIdAllocator>,
|
||||
controller: StageControllerHarness,
|
||||
controller: stage::StageControllerHarness,
|
||||
worker: MockWorker,
|
||||
event_cursor: usize,
|
||||
}
|
||||
|
|
@ -35,7 +34,7 @@ impl MockNode {
|
|||
inbound_edge: None,
|
||||
outbound_edge: None,
|
||||
outbound_object_allocator: None,
|
||||
controller: StageControllerHarness::new(stage::NodeId(node_id.0)),
|
||||
controller: stage::StageControllerHarness::new(stage::NodeId(node_id.0)),
|
||||
worker: MockWorker::new(stage_index, eos_after_sequence),
|
||||
event_cursor: 0,
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::run_plan as plan;
|
||||
use myelin::staging as stage;
|
||||
use mvp_system::staging as stage;
|
||||
|
||||
use super::mock_transport::{MockObject, MockObjectKind};
|
||||
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
mod harness;
|
||||
mod local_e2e_guarantees;
|
||||
mod local_mock;
|
||||
mod node_guarantees;
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
use crate::node_actor::{NodeAgentActor, NodeAgentMsg, NodeAgentReport};
|
||||
use crate::orchestration::actor::OrchestratorMsg;
|
||||
use iroh::{EndpointAddr, SecretKey};
|
||||
use myelin::staging as stage;
|
||||
use mvp_system::staging as stage;
|
||||
use swactor::actor::ActorAddress;
|
||||
use swactor::config::RuntimeConfig;
|
||||
use swactor::runtime::Runtime;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//! Behavior guarantees for the `observability` module.
|
||||
|
||||
//! Black-box contract tests for the Myelin observability surface.
|
||||
//! Black-box contract tests for the MVP observability surface.
|
||||
//!
|
||||
//! These tests intentionally know only the public event stream surface:
|
||||
//!
|
||||
|
|
@ -8,9 +8,9 @@
|
|||
//! - event identities, reason enums, and ordering observed by subscribers
|
||||
//!
|
||||
//! They assert the guarantees in
|
||||
//! `specs/myelin_system/observability_surface_contract.md`.
|
||||
//! `specs/mvp_system/observability_surface_contract.md`.
|
||||
|
||||
use myelin::observability::lifecycle as obs;
|
||||
use mvp_system::observability::lifecycle as obs;
|
||||
|
||||
// The trace fixture contains one successful run from boot through teardown.
|
||||
// Tests use structured events only; logs, transport, storage, and batching stay
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
//! Behavior guarantees for the `orchestration` module.
|
||||
|
||||
mod run_plan {
|
||||
//! Black-box contract tests for Myelin RunPlan formation.
|
||||
//! Black-box contract tests for MVP RunPlan formation.
|
||||
//!
|
||||
//! These tests intentionally know only the public planning surface:
|
||||
//!
|
||||
|
|
@ -205,7 +205,7 @@ mod run_plan {
|
|||
}
|
||||
}
|
||||
// This proves every edge has one public producer and one public consumer, and
|
||||
// that the returned edge graph is exactly the Myelin linear pipeline.
|
||||
// that the returned edge graph is exactly the MVP linear pipeline.
|
||||
#[test]
|
||||
fn edge_graph_is_exactly_the_linear_pipeline() {
|
||||
// Use four stages so the activation chain has multiple interior edges.
|
||||
|
|
@ -474,7 +474,7 @@ mod run_plan {
|
|||
input
|
||||
}
|
||||
|
||||
// Zero stages cannot form the Myelin pipeline. This fixture isolates the invalid
|
||||
// Zero stages cannot form the MVP pipeline. This fixture isolates the invalid
|
||||
// stage-count path without adding any other contradictory facts.
|
||||
fn invalid_zero_stage_count() -> PlannerInput {
|
||||
valid_input(0, 36)
|
||||
|
|
@ -491,7 +491,7 @@ mod run_plan {
|
|||
}
|
||||
|
||||
mod run_fsm {
|
||||
//! Black-box contract tests for the Myelin orchestrator run FSM.
|
||||
//! Black-box contract tests for the MVP orchestrator run FSM.
|
||||
//!
|
||||
//! These tests intentionally know only the public orchestrator surface:
|
||||
//!
|
||||
|
|
@ -502,7 +502,6 @@ mod run_fsm {
|
|||
//! `specs/BEHAVIOR_GUARANTEES.md`.
|
||||
|
||||
use crate::run_fsm as fsm;
|
||||
use crate::tests::harness::OrchestratorHarness;
|
||||
|
||||
// A three-stage plan proves multi-stage provisioning and readiness without
|
||||
// making tests depend on any placement heuristic. The plan is already valid;
|
||||
|
|
@ -530,8 +529,8 @@ mod run_fsm {
|
|||
// The harness is the black-box public boundary for the run FSM. It accepts
|
||||
// observable events and records emitted commands/events; tests never inspect an
|
||||
// internal FSM enum or private readiness counter.
|
||||
fn new_run() -> OrchestratorHarness {
|
||||
OrchestratorHarness::new(fsm::RunConfig {
|
||||
fn new_run() -> fsm::OrchestratorHarness {
|
||||
fsm::OrchestratorHarness::new(fsm::RunConfig {
|
||||
run_id: fsm::RunId(7),
|
||||
max_tokens: 4,
|
||||
prompt: vec![101, 102, 103],
|
||||
|
|
@ -681,6 +680,7 @@ mod run_fsm {
|
|||
});
|
||||
assert!(invalid.events().iter().any(|event| {
|
||||
matches!(event, fsm::LifecycleEvent::RunFaulted { .. })
|
||||
|| matches!(event, fsm::LifecycleEvent::RunRejected { .. })
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//! Behavior guarantees for the `prompt` module.
|
||||
|
||||
use myelin::prompt::rpc::{PromptEvent, SubmitPrompt};
|
||||
use mvp_system::prompt::rpc::{PromptEvent, SubmitPrompt};
|
||||
|
||||
#[test]
|
||||
fn zero_request_limits_take_loop_defaults() {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
//! Behavior guarantees for the `staging` module.
|
||||
|
||||
mod stage_controller {
|
||||
//! Black-box contract tests for Myelin StageController behavior.
|
||||
//! Black-box contract tests for MVP StageController behavior.
|
||||
//!
|
||||
//! These tests intentionally know only the public stage-controller surface:
|
||||
//!
|
||||
|
|
@ -11,8 +11,7 @@ mod stage_controller {
|
|||
//! They assert the guarantees in
|
||||
//! `specs/BEHAVIOR_GUARANTEES.md`.
|
||||
|
||||
use myelin::staging as stage;
|
||||
use crate::tests::harness::StageControllerHarness;
|
||||
use mvp_system::staging as stage;
|
||||
|
||||
// This provision fixture represents a single middle stage. It has one inbound
|
||||
// and one outbound edge so tests can prove the controller uses assigned edges
|
||||
|
|
@ -38,8 +37,8 @@ mod stage_controller {
|
|||
// The harness exposes only public messages. Tests intentionally do not inspect
|
||||
// private controller states such as "Preparing" or "Executing"; they infer
|
||||
// controller behavior from emitted commands and lifecycle events.
|
||||
fn new_controller() -> StageControllerHarness {
|
||||
StageControllerHarness::new(stage::NodeId(11))
|
||||
fn new_controller() -> stage::StageControllerHarness {
|
||||
stage::StageControllerHarness::new(stage::NodeId(11))
|
||||
}
|
||||
|
||||
// Preparation readiness has four independent prerequisites. Listing them as
|
||||
|
|
@ -61,7 +60,7 @@ mod stage_controller {
|
|||
// This helper provisions and readies a stage through public events. Tests that
|
||||
// focus on execution use it to avoid duplicating setup while still going through
|
||||
// the same observable path as production.
|
||||
fn ready_stage() -> StageControllerHarness {
|
||||
fn ready_stage() -> stage::StageControllerHarness {
|
||||
let mut harness = new_controller();
|
||||
harness.observe(stage::StageEvent::ProvisionStage {
|
||||
from: stage::NodeId(99),
|
||||
|
|
@ -104,6 +103,15 @@ mod stage_controller {
|
|||
)
|
||||
}));
|
||||
|
||||
// The controller must not emit any command that replaces the provisioned
|
||||
// edge ids with a locally chosen edge.
|
||||
assert!(
|
||||
!harness
|
||||
.commands()
|
||||
.iter()
|
||||
.any(|command| { matches!(command, stage::StageCommand::RewireEdge { .. }) })
|
||||
);
|
||||
|
||||
// An unauthorized provision attempt must fault before setup can begin.
|
||||
let mut unauthorized = new_controller();
|
||||
unauthorized.observe(stage::StageEvent::ProvisionStage {
|
||||
|
|
@ -201,7 +209,7 @@ mod stage_controller {
|
|||
assert_eq!(execute_steps[0].outputs[0].sequence, 0);
|
||||
|
||||
// A second object while the first step is active must not create another
|
||||
// active ExecuteStep in the Myelin.
|
||||
// active ExecuteStep in the MVP.
|
||||
harness.observe(stage::StageEvent::ObjectLoaded {
|
||||
edge_id: stage::EdgeId(7001),
|
||||
object_id: stage::ObjectId(9001),
|
||||
|
|
@ -511,7 +519,7 @@ mod stage_controller {
|
|||
}
|
||||
|
||||
mod weight_lifecycle {
|
||||
//! Black-box contract tests for Myelin stage-local weight lifecycle.
|
||||
//! Black-box contract tests for MVP stage-local weight lifecycle.
|
||||
//!
|
||||
//! These tests intentionally know only the public weight-work surface:
|
||||
//!
|
||||
|
|
@ -522,7 +530,7 @@ mod weight_lifecycle {
|
|||
//! They assert the guarantees in
|
||||
//! `specs/BEHAVIOR_GUARANTEES.md`.
|
||||
|
||||
use myelin::staging::weight_lifecycle as weights;
|
||||
use mvp_system::staging::weight_lifecycle as weights;
|
||||
|
||||
// A valid assignment gives the stage exactly one layer range and one source.
|
||||
// Tests vary only source or failure outcome so the assignment contract remains
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue