diff --git a/RFC-0019-connector-plugins.md b/RFC-0019-connector-plugins.md new file mode 100644 index 00000000..ac261928 --- /dev/null +++ b/RFC-0019-connector-plugins.md @@ -0,0 +1,226 @@ +# RFC-0019 for Presto + +## Connector Plugin Support for Presto C++ Workers + +Proposers +* Tim Meehan + +## Related Issues + +* [RFC-0013](./RFC-0013-dyllib-cpp.md) - Introduced the `registerExtensions` mechanism for dynamically loaded functions, which this RFC extends for connectors +* [RFC #38](https://github.com/prestodb/rfcs/pull/38) - Introduces the Java and C++ codec infrastructure for connector-provided serialization in the context of Thrift serialization for coordinator to worker communication +* [PR #26026](https://github.com/prestodb/presto/pull/26026) - Builds on RFC #38 to enable binary serialization codecs for JSON protocol (avoiding Thrift migration) and adds missing codecs embedded in TaskUpdateRequest + +## Summary + +Enable runtime connector registration in Presto C++ workers through connector plugins loaded as shared libraries using protocol-agnostic binary serialization. This allows developers to use the existing `registerExtensions` mechanism to add custom Velox connectors while providing the ability to decode Java-side connector structures into corresponding Velox structures. + +## Background + +Presto C++ workers require all connectors at compile time. The protocol generation statically creates C++ classes from Java definitions, requiring connector-specific data structures in the worker binary. This prevents development of new connectors not known to the Presto engine, representing a significant regression from Java's functionality. + +Java coordinators support connector plugin registration with both JSON and Thrift protocols. [PR #26026](https://github.com/prestodb/presto/pull/26026)[^1] adds binary serialization codec support to the JSON protocol. + +### Goals + +* Enable runtime connector registration in C++ workers + +### Non-goals + +* Cross-language plugin support (Rust, Go) +* Hot-reload capabilities + +## Proposed Implementation + +### Modules Involved + +1. **presto-native-execution** + +### Components + +This RFC leverages existing infrastructure: +* **ConnectorProtocol**: Existing interface that plugins implement with binary serialization for their connector types[^2] +* **PrestoToVeloxConnector**: Existing class that creates the ConnectorProtocol and converts protocol types to Velox types +* **registerExtensions**: Existing mechanism from RFC-0013 used to register connector components from shared libraries + +### Implementation Flow + +1. **Plugin Registration** (using existing `registerExtensions` mechanism): + ```cpp + extern "C" void registerExtensions() { + // Register Presto-to-Velox converter (which creates the protocol) + auto converter = std::make_unique(); + registerPrestoToVeloxConnector("my-connector", std::move(converter)); + + // Register Velox connector factory + velox::connector::registerConnectorFactory("my-connector", factory); + } + ``` + +2. **Message Routing**: + - Coordinator includes connector name in the serialized payload (embedded in handle objects) + - Worker extracts connector name from the deserialized handle to identify which ConnectorProtocol to use + - ConnectorManager maintains registry mapping connector names to their protocols: + * Static connectors → pre-compiled protocol classes registered at startup + * Plugin connectors → ConnectorProtocol implementations from shared libraries + +3. **Binary Serialization Protocol**[^3]: + - JSON (via [PR #26026](https://github.com/prestodb/presto/pull/26026)) and Thrift support binary payloads + - ConnectorCodecProvider (Java) ↔ ConnectorProtocol (C++) + +4. **C++ Worker Configuration**[^4]: + ```properties + # config.properties for C++ worker + plugin.dir=/opt/presto/plugin # Optional, defaults to ./plugin + ``` + Worker scans plugin directory at startup and loads all shared libraries (.so/.dylib files) using dlopen. + +### Architecture Diagram + +![Architecture Diagram](RFC-0019/architecture-diagram.png) + +### Interface Contracts + +Each plugin provides its own ConnectorProtocol implementation with connector-specific binary serialization. Worker loads plugins using the mechanism introduced in [RFC-0013](./RFC-0013-dyllib-cpp.md) (and documented [here](https://prestodb.io/docs/current/presto_cpp/plugin/function_plugin.html)), where shared libraries are loaded via dlopen/dlsym at startup based on configuration. + +## Metrics + +* N/A + +## Other Approaches Considered + +N/A + +## Adoption Plan + +* **Impact**: None on existing users. Static connectors unchanged. +* **New configurations**: `plugin.dir` property in C++ worker config.properties +* **Migration**: Gradual per-connector basis +* **Documentation**: Plugin development guide, example implementations +* **Out of scope**: Cross-language support, hot-reload + +## Test Plan + +1. **Unit tests**: + - ConnectorProtocol serialization correctness + - Plugin loading/unloading + - Error handling + +2. **Integration tests**: + - End-to-end queries with plugin connectors + - Mixed static/plugin queries + - Failure scenarios + +## Implementation Considerations + +### Security + +Shared libraries must be trusted code (same as Java plugins). + +### Performance + +Virtual function overhead negligible compared to network I/O. + +### Compatibility + +Plugins require same toolchain as worker binary for ABI compatibility. The Java coordinator must enable `use-connector-provided-serialization-codecs` and have a complete implementation of the binary codec for the connector. Both coordinator and worker need matching SerializationCodec implementations. + +### Limitations + +C++ ABI compatibility requires matching compiler versions and platform. + +[^1]: [PR #26026](https://github.com/prestodb/presto/pull/26026) enables binary serialization in JSON protocol via ConnectorCodecProvider. + +[^2]: ConnectorProtocol interface that plugins implement: +```cpp +class ConnectorProtocol { +public: + // Plugins implement these for binary serialization + virtual void serialize(const std::shared_ptr& proto, + std::string& binary) const = 0; + virtual void deserialize(const std::string& binary, + std::shared_ptr& proto) const = 0; + + // JSON/Thrift methods + virtual void to_json(json& j, const std::shared_ptr& p) const; + virtual void from_json(const json& j, std::shared_ptr& p) const; +}; +``` + +[^3]: Protocol extensions: +```thrift +struct PluginConnectorData { + 1: required string connectorName + 2: required binary serializedData + 3: optional map metadata + 4: required i32 protocolVersion +} + +union ConnectorSplitData { + 1: HiveSplit hiveSplit + 2: IcebergSplit icebergSplit + 3: PluginConnectorData pluginData +} +``` +JSON with binary payload: +```json +{ + "@type": "ConnectorSplit", + "connectorId": "my-connector", + "serializedData": "", + "metadata": {"protocolVersion": "1"} +} +``` + +[^4]: C++ worker configuration and plugin example: +```properties +# config.properties for C++ worker +plugin.dir=/opt/presto/plugin # Optional, defaults to ./plugin +``` +Example plugin: +```cpp +// Step 1: Protocol deserialization - handles binary to protocol type +class ArrowConnectorProtocol final : public ConnectorProtocol { + public: + void deserialize( + const std::string& binaryData, + std::shared_ptr& proto) const override; + + void to_json(json& j, const std::shared_ptr& p) + const override { + VELOX_NYI("JSON not supported with binary serialization"); + } + + /* ... Other deserialization methods, serialization methods throw NYI errors ... */ +}; + +// Step 2: PrestoToVeloxConnector - creates protocol and converts types +class ArrowFlightPrestoToVeloxConnector : public PrestoToVeloxConnector { +public: + std::unique_ptr createConnectorProtocol() const override { + return std::make_unique(); + } + + std::unique_ptr toVeloxSplit( + const std::shared_ptr& prestoSplit) const override { + auto arrowSplit = std::dynamic_pointer_cast(prestoSplit); + // Convert protocol ArrowFlightSplit to Velox ArrowFlightConnectorSplit + return std::make_unique( + arrowSplit->ticket, + arrowSplit->endpoint); + } +}; + +extern "C" { + void registerExtensions() { + // Register Presto-to-Velox converter + auto converter = std::make_unique(); + registerPrestoToVeloxConnector("arrow-flight", std::move(converter)); + + // Register Velox connector factory + velox::connector::registerConnectorFactory( + "arrow-flight", + std::make_unique()); + } +} +``` \ No newline at end of file diff --git a/RFC-0019/architecture-diagram.excalidraw b/RFC-0019/architecture-diagram.excalidraw new file mode 100644 index 00000000..b6a89ee3 --- /dev/null +++ b/RFC-0019/architecture-diagram.excalidraw @@ -0,0 +1,888 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw-jetbrains-plugin", + "elements": [ + { + "type": "rectangle", + "version": 4, + "versionNonce": 1970594961, + "isDeleted": false, + "id": "coordinator-box", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 100, + "y": 50, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 600, + "height": 150, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "a0" + }, + { + "type": "text", + "version": 57, + "versionNonce": 1389178961, + "isDeleted": false, + "id": "coordinator-label", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 306.0300750732422, + "y": 69.87890625, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 187.93984985351562, + "height": 25, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327051278, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Coordinator (Java)", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Coordinator (Java)", + "lineHeight": 1.25, + "baseline": 18, + "index": "a1", + "autoResize": true + }, + { + "type": "rectangle", + "version": 4, + "versionNonce": 1735142001, + "isDeleted": false, + "id": "codec-provider-box", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 150, + "y": 120, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "width": 500, + "height": 60, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "a2" + }, + { + "type": "text", + "version": 6, + "versionNonce": 1714987217, + "isDeleted": false, + "id": "codec-provider-label", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 222.3361053466797, + "y": 140, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 355.3277893066406, + "height": 20, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327050163, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 1, + "text": "ConnectorCodecProvider (Binary Serialization)", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "ConnectorCodecProvider (Binary Serialization)", + "lineHeight": 1.25, + "baseline": 14, + "index": "a3", + "autoResize": true + }, + { + "type": "arrow", + "version": 4, + "versionNonce": 171947089, + "isDeleted": false, + "id": "transport-arrow", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 400, + "y": 200, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0, + "height": 80, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 80 + ] + ], + "index": "a4" + }, + { + "type": "text", + "version": 5, + "versionNonce": 816578353, + "isDeleted": false, + "id": "transport-label", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 420, + "y": 230, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 171.087890625, + "height": 20, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327048393, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 1, + "text": "JSON/Thrift Protocol", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "JSON/Thrift Protocol", + "lineHeight": 1.25, + "baseline": 14, + "index": "a5", + "autoResize": true + }, + { + "type": "rectangle", + "version": 4, + "versionNonce": 29819441, + "isDeleted": false, + "id": "worker-box", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 100, + "y": 300, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffd8a8", + "width": 600, + "height": 400, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "a6" + }, + { + "type": "text", + "version": 47, + "versionNonce": 601046527, + "isDeleted": false, + "id": "worker-label", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 336.4611358642578, + "y": 319.87109375, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 126.81991577148438, + "height": 25, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327053340, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Worker (C++)", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Worker (C++)", + "lineHeight": 1.25, + "baseline": 18, + "index": "a7", + "autoResize": true + }, + { + "type": "rectangle", + "version": 4, + "versionNonce": 183079953, + "isDeleted": false, + "id": "protocol-registry-box", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 150, + "y": 360, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "width": 500, + "height": 120, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "a8" + }, + { + "type": "text", + "version": 22, + "versionNonce": 1359836543, + "isDeleted": false, + "id": "protocol-registry-label", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 331.7200469970703, + "y": 370, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 136.55990600585938, + "height": 20, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327052578, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 1, + "text": "Protocol Registry", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Protocol Registry", + "lineHeight": 1.25, + "baseline": 14, + "index": "a9", + "autoResize": true + }, + { + "type": "rectangle", + "version": 4, + "versionNonce": 1503586801, + "isDeleted": false, + "id": "hive-protocol-box", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 170, + "y": 410, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "width": 140, + "height": 50, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "aA" + }, + { + "type": "text", + "version": 6, + "versionNonce": 1703042225, + "isDeleted": false, + "id": "hive-protocol-label", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 192.61602783203125, + "y": 425, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 94.7679443359375, + "height": 20, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327055628, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 1, + "text": "HiveProtocol", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "HiveProtocol", + "lineHeight": 1.25, + "baseline": 14, + "index": "aB", + "autoResize": true + }, + { + "type": "rectangle", + "version": 4, + "versionNonce": 921600977, + "isDeleted": false, + "id": "iceberg-protocol-box", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 330, + "y": 410, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffffff", + "width": 140, + "height": 50, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "aC" + }, + { + "type": "text", + "version": 6, + "versionNonce": 482989855, + "isDeleted": false, + "id": "iceberg-protocol-label", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 338.696044921875, + "y": 425, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 122.60791015625, + "height": 20, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327056476, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 1, + "text": "IcebergProtocol", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "IcebergProtocol", + "lineHeight": 1.25, + "baseline": 14, + "index": "aD", + "autoResize": true + }, + { + "type": "rectangle", + "version": 2, + "versionNonce": 1592779185, + "isDeleted": false, + "id": "plugin-protocol-box", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 490, + "y": 410, + "strokeColor": "#1e1e1e", + "backgroundColor": "#d0bfff", + "width": 140, + "height": 50, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "aE" + }, + { + "type": "text", + "version": 3, + "versionNonce": 309719007, + "isDeleted": false, + "id": "plugin-protocol-label", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 505.90403747558594, + "y": 425, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 108.19192504882812, + "height": 20, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327042883, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 1, + "text": "PluginProtocol", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "PluginProtocol", + "lineHeight": 1.25, + "baseline": 14, + "index": "aF", + "autoResize": true + }, + { + "type": "arrow", + "version": 2, + "versionNonce": 516588433, + "isDeleted": false, + "id": "plugin-to-plugins-arrow", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 560, + "y": 460, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0, + "height": 40, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 40 + ] + ], + "index": "aG" + }, + { + "type": "rectangle", + "version": 4, + "versionNonce": 1807612415, + "isDeleted": false, + "id": "plugins-box", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 150, + "y": 520, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "width": 500, + "height": 120, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "aH" + }, + { + "type": "text", + "version": 78, + "versionNonce": 724616831, + "isDeleted": false, + "id": "plugins-label", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 311.3920593261719, + "y": 529.32421875, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 177.21588134765625, + "height": 20, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327054496, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 1, + "text": "Connector Plugins (.so)", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Connector Plugins (.so)", + "lineHeight": 1.25, + "baseline": 14, + "index": "aI", + "autoResize": true + }, + { + "type": "rectangle", + "version": 2, + "versionNonce": 1795973663, + "isDeleted": false, + "id": "plugin-a-box", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 170, + "y": 570, + "strokeColor": "#1e1e1e", + "backgroundColor": "#d0bfff", + "width": 140, + "height": 50, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "aJ" + }, + { + "type": "text", + "version": 3, + "versionNonce": 320201008, + "isDeleted": false, + "id": "plugin-a-label", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 209.3200225830078, + "y": 585, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 61.359954833984375, + "height": 20, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327108694, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 1, + "text": "Plugin A", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Plugin A", + "lineHeight": 1.25, + "baseline": 14, + "index": "aK", + "autoResize": true + }, + { + "type": "rectangle", + "version": 2, + "versionNonce": 546883135, + "isDeleted": false, + "id": "plugin-b-box", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 330, + "y": 570, + "strokeColor": "#1e1e1e", + "backgroundColor": "#d0bfff", + "width": 140, + "height": 50, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "aL" + }, + { + "type": "text", + "version": 3, + "versionNonce": 983042512, + "isDeleted": false, + "id": "plugin-b-label", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 368.7520217895508, + "y": 585, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 62.49595642089844, + "height": 20, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327108694, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 1, + "text": "Plugin B", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Plugin B", + "lineHeight": 1.25, + "baseline": 14, + "index": "aM", + "autoResize": true + }, + { + "type": "rectangle", + "version": 2, + "versionNonce": 188982879, + "isDeleted": false, + "id": "plugin-c-box", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 490, + "y": 570, + "strokeColor": "#1e1e1e", + "backgroundColor": "#d0bfff", + "width": 140, + "height": 50, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1759327036209, + "link": null, + "locked": false, + "index": "aN" + }, + { + "type": "text", + "version": 3, + "versionNonce": 1646410544, + "isDeleted": false, + "id": "plugin-c-label", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 529.4160232543945, + "y": 585, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 61.16795349121094, + "height": 20, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1759327108694, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 1, + "text": "Plugin C", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Plugin C", + "lineHeight": 1.25, + "baseline": 14, + "index": "aO", + "autoResize": true + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/RFC-0019/architecture-diagram.png b/RFC-0019/architecture-diagram.png new file mode 100644 index 00000000..947e44c6 Binary files /dev/null and b/RFC-0019/architecture-diagram.png differ