diff --git a/docker-configs/docker-compose.yml b/docker-configs/docker-compose.yml index ae174d0..f08907b 100644 --- a/docker-configs/docker-compose.yml +++ b/docker-configs/docker-compose.yml @@ -24,7 +24,7 @@ services: mem_reservation: 600m volumes: - '/etc/freedmr/freedmr.cfg:/opt/freedmr/freedmr.cfg' - # HDSTACK=2..5 requires the separate bridge inputs below. + # HDSTACK=2..5 requires these unless HDSTACK_BRIDGE=0. #- '/etc/freedmr/freedmr-bridge.cfg:/opt/freedmr/freedmr-bridge.cfg:ro' #- '/etc/freedmr/rules-bridge.py:/opt/freedmr/rules.py:ro' #Write JSON files outside of container @@ -54,6 +54,8 @@ services: # Optional 2..5 worker HDStack deployment. Omit for current mode. #- HDSTACK=2 #- HDSTACK_BASEID=23400 + # Disable the separate bridge.py process when it is not required. + #- HDSTACK_BRIDGE=0 #- HDSTACK_BRIDGE_CONFIG=/opt/freedmr/freedmr-bridge.cfg #- HDSTACK_BRIDGE_RULES=/opt/freedmr/rules.py tmpfs: /tmp diff --git a/docker-configs/entrypoint-proxy b/docker-configs/entrypoint-proxy index 9ef1d4a..f6cb864 100755 --- a/docker-configs/entrypoint-proxy +++ b/docker-configs/entrypoint-proxy @@ -20,6 +20,7 @@ cd /opt/freedmr HDSTACK_COUNT="${HDSTACK:-1}" +HDSTACK_BRIDGE="${HDSTACK_BRIDGE:-1}" HDSTACK_OUTPUT_DIR="${HDSTACK_OUTPUT_DIR:-/dev/shm/freedmr-hdstack}" HDSTACK_BRIDGE_CONFIG="${HDSTACK_BRIDGE_CONFIG:-/opt/freedmr/freedmr-bridge.cfg}" HDSTACK_BRIDGE_RULES="${HDSTACK_BRIDGE_RULES:-/opt/freedmr/rules.py}" @@ -52,7 +53,8 @@ then --bridge-rules "$HDSTACK_BRIDGE_RULES" \ --output-dir "$HDSTACK_OUTPUT_DIR" \ --instances "$HDSTACK_COUNT" \ - --base-id "$HDSTACK_BASEID" + --base-id "$HDSTACK_BASEID" \ + --bridge-enabled "$HDSTACK_BRIDGE" status=$? if [ "$status" -ne 0 ] then diff --git a/docs/hdstack.md b/docs/hdstack.md index fc04ed9..b032b37 100644 --- a/docs/hdstack.md +++ b/docs/hdstack.md @@ -11,13 +11,13 @@ selects the generated multi-process layout: ```text hotspots -> proxy -> HBP workers -> no-HBP aggregator -> external FBP/OBP | - +-> separate bridge.py instance + +-> optional separate bridge.py instance worker reports -> reporting MUX -> existing dashboard/reporting consumer ``` The aggregator owns all OpenBridge/FBP links from the main `freedmr.cfg` and -has no HBP `MASTER`, `PEER` or `XLXPEER` system. The separate `bridge.py` +has no HBP `MASTER`, `PEER` or `XLXPEER` system. The optional `bridge.py` process retains its own configuration and rules. The reporting MUX reads only worker reports; aggregator and bridge reporting are not included. @@ -36,7 +36,20 @@ adding their one-based worker number. For the example above, the aggregator is `23400` and the two workers are `23401` and `23402`. With `HDSTACK=5`, worker IDs continue through `23405`. -Multi-process mode also requires the existing separate bridge inputs: +The separate `bridge.py` process is enabled by default. Disable it when the +deployment does not require bridge rules or HBP peer/XLX connections: + +```yaml +environment: + - HDSTACK_BRIDGE=0 +``` + +`HDSTACK_BRIDGE` accepts only `0` or `1`. It affects only multi-process mode. +When disabled, no bridge configuration or rules file is required and no +`bridge.py` process is started. Remove or disable any aggregator OpenBridge +section intended only to connect to that bridge instance. + +When the bridge is enabled, multi-process mode requires its existing inputs: ```yaml volumes: @@ -65,10 +78,11 @@ derived. Worker and aggregator configuration files are generated privately in `/dev/shm/freedmr-hdstack` when the container starts. The mounted source configuration and bridge files are not changed. -The bridge configuration must contain the reciprocal link to the aggregator -and any `PEER` or `XLXPEER` systems used by `bridge.py`. Its `rules.py` remains -the authority for bridge routing. Bridge reporting must either be disabled or -use a port which does not collide with the worker reports or MUX. +When enabled, the bridge configuration must contain the reciprocal link to the +aggregator and any `PEER` or `XLXPEER` systems used by `bridge.py`. Its +`rules.py` remains the authority for bridge routing. Bridge reporting must +either be disabled or use a port which does not collide with the worker reports +or MUX. ## Worker ports diff --git a/hdstack_runtime.py b/hdstack_runtime.py index 121cecc..48ab1e4 100644 --- a/hdstack_runtime.py +++ b/hdstack_runtime.py @@ -49,17 +49,23 @@ def generate_hdstack( output_dir, instances, base_server_id, + bridge_enabled=True, ): - """Generate configs for an aggregator, workers, bridge, proxy and MUX.""" + """Generate configs for an aggregator, workers, optional bridge, proxy and MUX.""" instances = _validated_instances(instances) base_server_id = _validated_base_id(base_server_id, instances) + bridge_enabled = _validated_bridge_enabled(bridge_enabled) main_path = _required_file(main_config, 'main FreeDMR configuration') - bridge_path = _required_file(bridge_config, 'bridge configuration') - rules_path = _required_file(bridge_rules, 'bridge rules') + bridge_path = None + rules_path = None + bridge_source = None + if bridge_enabled: + bridge_path = _required_file(bridge_config, 'bridge configuration') + rules_path = _required_file(bridge_rules, 'bridge rules') + bridge_source = _read_config(bridge_path) output_path = Path(output_dir) source = _read_config(main_path) - bridge_source = _read_config(bridge_path) system = _system_template(source) hbp_port = _integer(system, 'PORT', 54000, 'SYSTEM PORT') generator_size = _integer(system, 'GENERATOR', 100, 'SYSTEM GENERATOR') @@ -95,6 +101,7 @@ def generate_hdstack( report_port, worker_report_ports, instances, + bridge_enabled, ) aggregator = _support_config(source) @@ -169,6 +176,7 @@ def generate_hdstack( report_port, worker_report_ports, _report_clients(source), + bridge_enabled, ), encoding='utf-8', ) @@ -207,6 +215,12 @@ def _validated_base_id(value, instances): return value +def _validated_bridge_enabled(value): + if not isinstance(value, bool): + raise HDStackConfigurationError('HDSTACK_BRIDGE must be 0 or 1') + return value + + def _required_file(value, description): path = Path(value) if not path.is_file(): @@ -316,6 +330,7 @@ def _validate_generated_port_collisions( report_port, worker_report_ports, instances, + bridge_enabled, ): generated = {} @@ -346,7 +361,9 @@ def _validate_generated_port_collisions( reserve(port, 'worker {} reporting'.format(number)) configured = _enabled_bind_ports(source, excluded_sections=('SYSTEM', 'ECHO')) - bridge_ports = _enabled_bind_ports(bridge_source) + bridge_ports = {} + if bridge_enabled: + bridge_ports = _enabled_bind_ports(bridge_source) for port, sections in configured.items(): if port in generated: raise HDStackConfigurationError( @@ -366,7 +383,7 @@ def _validate_generated_port_collisions( ) ) - if _boolean(bridge_source, 'REPORTS', 'REPORT', True): + if bridge_enabled and _boolean(bridge_source, 'REPORTS', 'REPORT', True): bridge_report_port = _section_integer( bridge_source, 'REPORTS', @@ -463,6 +480,7 @@ def _supervisor_config( report_port, worker_report_ports, report_clients, + bridge_enabled, ): parts = [ '[supervisord]\n' @@ -488,15 +506,18 @@ def _supervisor_config( 20, ) ) + if bridge_enabled: + parts.append( + _program( + 'bridge', + 'python /opt/freedmr/bridge.py -c {} -r {}'.format( + shlex.quote(str(bridge_path)), + shlex.quote(str(rules_path)), + ), + 30, + ) + ) parts.extend([ - _program( - 'bridge', - 'python /opt/freedmr/bridge.py -c {} -r {}'.format( - shlex.quote(str(bridge_path)), - shlex.quote(str(rules_path)), - ), - 30, - ), _program( 'playback', '/opt/freedmr/playback.py -c /opt/freedmr/loro.cfg', @@ -544,6 +565,7 @@ def main(argv=None): parser.add_argument('--output-dir', required=True) parser.add_argument('--instances', required=True, type=int) parser.add_argument('--base-id', required=True, type=int) + parser.add_argument('--bridge-enabled', choices=('0', '1'), default='1') args = parser.parse_args(argv) try: generate_hdstack( @@ -553,6 +575,7 @@ def main(argv=None): args.output_dir, args.instances, args.base_id, + bridge_enabled=args.bridge_enabled == '1', ) except HDStackConfigurationError as err: parser.error(str(err)) diff --git a/tests/test_hdstack.py b/tests/test_hdstack.py index 473bf2d..16ddfb4 100644 --- a/tests/test_hdstack.py +++ b/tests/test_hdstack.py @@ -23,7 +23,7 @@ class HDStackRuntimeTests(unittest.TestCase): self.bridge_config.write_text(BRIDGE_CONFIG, encoding='utf-8') self.bridge_rules.write_text('BRIDGES = {}\n', encoding='utf-8') - def generate(self, instances=2, base_id=23400): + def generate(self, instances=2, base_id=23400, bridge_enabled=True): return hdstack_runtime.generate_hdstack( self.main_config, self.bridge_config, @@ -31,6 +31,7 @@ class HDStackRuntimeTests(unittest.TestCase): self.root / 'generated', instances, base_id, + bridge_enabled=bridge_enabled, ) def test_two_worker_layout_uses_base_id_plus_one_and_disjoint_ranges(self): @@ -183,6 +184,24 @@ class HDStackRuntimeTests(unittest.TestCase): supervisor, ) + def test_bridge_can_be_disabled_without_bridge_files(self): + self.bridge_config.unlink() + self.bridge_rules.unlink() + + layout = self.generate(bridge_enabled=False) + supervisor = layout.supervisor_config.read_text(encoding='utf-8') + + self.assertNotIn('[program:bridge]', supervisor) + for program in ( + 'hdstack-aggregator', + 'hdstack-worker-1', + 'hdstack-worker-2', + 'playback', + 'proxy', + 'report-mux', + ): + self.assertIn('[program:{}]'.format(program), supervisor) + def test_reporting_disabled_omits_mux_and_worker_reporting(self): self.main_config.write_text( MAIN_CONFIG.replace('REPORT: True', 'REPORT: False'), @@ -251,6 +270,9 @@ class HDStackRuntimeTests(unittest.TestCase): with self.assertRaises(hdstack_runtime.HDStackConfigurationError): self.generate() + with self.assertRaises(hdstack_runtime.HDStackConfigurationError): + self.generate(bridge_enabled=1) + def test_missing_bridge_files_and_port_collisions_fail_before_generation(self): self.bridge_rules.unlink() with self.assertRaises(hdstack_runtime.HDStackConfigurationError): @@ -274,6 +296,8 @@ class HDStackContainerTests(unittest.TestCase): self.assertIn('[ "$HDSTACK_COUNT" -ge 2 ]', entrypoint) self.assertIn('[ "$HDSTACK_COUNT" -le 5 ]', entrypoint) self.assertIn('--base-id "$HDSTACK_BASEID"', entrypoint) + self.assertIn('HDSTACK_BRIDGE="${HDSTACK_BRIDGE:-1}"', entrypoint) + self.assertIn('--bridge-enabled "$HDSTACK_BRIDGE"', entrypoint) self.assertIn('HDSTACK_BRIDGE_CONFIG', entrypoint) self.assertIn('HDSTACK_BRIDGE_RULES', entrypoint) @@ -282,6 +306,7 @@ class HDStackContainerTests(unittest.TestCase): self.assertIn('#- HDSTACK=2', compose) self.assertIn('#- HDSTACK_BASEID=23400', compose) + self.assertIn('#- HDSTACK_BRIDGE=0', compose) self.assertIn('#- HDSTACK_BRIDGE_CONFIG=', compose) self.assertIn('#- HDSTACK_BRIDGE_RULES=', compose) self.assertIn(