Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a756778
build, test, doc with 'latest' container
iboyd-ansys Jul 7, 2026
a2fa6ff
enable 'latest' unit test
iboyd-ansys Jul 15, 2026
b636056
temp disable most unit tests
iboyd-ansys Jul 15, 2026
4ca68e2
add doc/container diagnostics
iboyd-ansys Jul 15, 2026
c2f4f53
chore: adding changelog file 681.added.md [dependabot-skip]
pyansys-ci-bot Jul 15, 2026
90261b9
extend diagnostics
iboyd-ansys Jul 15, 2026
e745f1d
extend diagnostics (2)
iboyd-ansys Jul 15, 2026
caa1fb7
add timeouts to docker compose subprocess calls in conf.py
iboyd-ansys Jul 16, 2026
2a053f3
fix deadlock in ParticipantManager when SyC gRPC fails mid-solve
iboyd-ansys Jul 16, 2026
88e5e71
add debug logging to sphinx gallery _reset_example for docker contain…
iboyd-ansys Jul 16, 2026
1cbbed7
add background diagnostics collection to CI to capture state during h…
iboyd-ansys Jul 16, 2026
285214e
enable DEBUG logging in sphinx gallery examples for instrumentation d…
iboyd-ansys Jul 16, 2026
706507b
fix logging import and apply v27.1-specific more passive gRPC settings
iboyd-ansys Jul 16, 2026
f4f002b
enhance diagnostics for output streaming and server working directory…
iboyd-ansys Jul 17, 2026
eb4f74b
add detailed output stream chunk analysis to identify content and flu…
iboyd-ansys Jul 17, 2026
7eea0a1
move diagnostics monitor inside Build HTML step for visibility on can…
iboyd-ansys Jul 17, 2026
2cae8f8
upgrade MAPDL image from v25.2-ubuntu-cicd to v26.1.0
iboyd-ansys Jul 17, 2026
5552d40
revert MAPDL to v25.2-ubuntu-cicd: v26.1.0 has connection compatibili…
iboyd-ansys Jul 17, 2026
1e49cbd
adjust install steps
iboyd-ansys Jul 20, 2026
4dc6740
revert previous change
iboyd-ansys Jul 20, 2026
99a2a56
expose install output
iboyd-ansys Jul 20, 2026
bd00dcd
remove -q flag from CI doc install step
iboyd-ansys Jul 20, 2026
5c23ad6
add System Coupling server-side logging support
iboyd-ansys Jul 20, 2026
e5e8751
revert to 25.2 Fluent container and PyFluent 0.35.0
iboyd-ansys Jul 20, 2026
920f1c6
preserve containers for debugging with PYSYC_PRESERVE_CONTAINER env var
iboyd-ansys Jul 20, 2026
aae2cc0
update pyfluent too
iboyd-ansys Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
363 changes: 250 additions & 113 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions doc/changelog.d/681.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Chore: build using \"latest\" container
93 changes: 85 additions & 8 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,22 @@ def backup_folder():


def _reset_example(gallery_conf, fname: str, when: str):
import logging
import time

example_name = fname.replace(".py", "")

# Enable DEBUG logging for all examples to capture instrumentation diagnostics.
# This must be set early before examples create sessions/threads.
if when == "before":
from ansys.systemcoupling.core.util.logging import LOG

try:
LOG.set_level(logging.DEBUG)
print("[_reset_example] DEBUG logging enabled for diagnostics")
except Exception as exc:
print(f"[_reset_example] Warning: Could not enable DEBUG logging: {exc}")

# Add any examples that need MAPDL to this list
using_mapdl_examples = ["oscillating_plate", "turek_hron_fsi2", "cht_pipe"]

Expand All @@ -232,19 +246,82 @@ def _reset_example(gallery_conf, fname: str, when: str):
_clean_up_example_folder("00-systemcoupling", example_name)

if using_mapdl_container:
subprocess.run(
["docker", "compose", "-f", "mapdl-docker-compose.yml", "up", "-d"]
print(
f"[_reset_example] BEFORE example '{example_name}': "
f"launching MAPDL container..."
)
print("MAPDL container launched")
start_time = time.time()
try:
subprocess.run(
["docker", "compose", "-f", "mapdl-docker-compose.yml", "up", "-d"],
timeout=120,
)
elapsed = time.time() - start_time
print(
f"[_reset_example] MAPDL container launched successfully "
f"({elapsed:.1f}s)"
)
except subprocess.TimeoutExpired:
print(
f"[_reset_example] TIMEOUT after 120s while launching MAPDL "
f"container"
)
raise
except Exception as exc:
print(
f"[_reset_example] Exception launching MAPDL container: "
f"{type(exc).__name__}: {exc}"
)
raise
else:
if using_mapdl_container:
subprocess.run(
["docker", "compose", "-f", "mapdl-docker-compose.yml", "down"]
print(
f"[_reset_example] AFTER example '{example_name}': "
f"stopping MAPDL container..."
)
print("MAPDL container removed")
# Add sleep after example to see if it helps with grpcs errors seen after everything
# should have finished.
# Pass --timeout to docker compose to force-kill after 30s if graceful
# shutdown stalls (e.g. after an abrupt gRPC disconnect mid-solve).
# Also pass timeout to subprocess.run as a safety net in case docker
# compose itself hangs.
start_time = time.time()
try:
subprocess.run(
[
"docker",
"compose",
"-f",
"mapdl-docker-compose.yml",
"down",
"--timeout",
"30",
],
timeout=60,
)
elapsed = time.time() - start_time
print(
f"[_reset_example] MAPDL container stopped successfully "
f"({elapsed:.1f}s)"
)
except subprocess.TimeoutExpired:
print(
f"[_reset_example] TIMEOUT after 60s while stopping MAPDL "
f"container"
)
raise
except Exception as exc:
print(
f"[_reset_example] Exception stopping MAPDL container: "
f"{type(exc).__name__}: {exc}"
)
raise
# Add sleep after example to see if it helps with grpcs errors seen after
# everything should have finished.
print(
f"[_reset_example] AFTER example '{example_name}': sleeping 10s "
f"before cleanup..."
)
time.sleep(10)
print(f"[_reset_example] AFTER example '{example_name}': cleanup complete")


rst_epilog = make_replacements_for_versioned_class_refs(("CASE", "SETUP", "SOLUTION"))
Expand Down
3 changes: 3 additions & 0 deletions examples/00-systemcoupling/cht_pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@
target_variable="HFLW",
)

# TEMP: disable AnsRpcBridge
syc.setup.coupling_participant[solid_name].use_ans_rpc_bridge = False

# %%
# Define constants and calculate Biot number
# ------------------------------------------
Expand Down
15 changes: 15 additions & 0 deletions src/ansys/systemcoupling/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ def launch(
an argument has an associated value, the argument name and its
value should be specified as two consecutive items of the list.

Notes
-----
Environment variables:
- ``PYSYC_SERVER_LOGGING_LEVEL``: Enable System Coupling server-side logging
by specifying a logging level (e.g., ``5`` for verbose output). Log files
will be written to the working directory as ``SyC_Log_*.txt``.

Returns
-------
ansys.systemcoupling.core.session.Session
Expand All @@ -110,6 +117,14 @@ def launch(
"""
rpc = SycGrpc()
version = str(version) if version is not None else None

# Check for server-side logging level via environment variable
# e.g., PYSYC_SERVER_LOGGING_LEVEL=5 will add "-l 5" to launch args
server_logging_level = os.environ.get("PYSYC_SERVER_LOGGING_LEVEL")
if server_logging_level:
extra_args = list(extra_args) + ["-l", str(server_logging_level)]
LOG.info(f"System Coupling server logging level: {server_logging_level}")

if pypim.is_configured():
LOG.info(
"Starting System Coupling remotely. Any launch arguments other "
Expand Down
133 changes: 120 additions & 13 deletions src/ansys/systemcoupling/core/client/grpc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,36 +523,143 @@ def start_output(self, handle_output=None):
Standard output and error streams are combined in the output
streamed to this client.
"""
LOG.debug("[start_output] ENTER: Starting output streaming")

def default_handler(text):
import sys

# Log to ensure this is being called (first 5 times, then every 100)
if not hasattr(default_handler, "_call_count"):
default_handler._call_count = 0
default_handler._call_count += 1

if (
default_handler._call_count <= 5
or default_handler._call_count % 100 == 0
):
text_repr = repr(text[:80]) if len(text) > 0 else "<empty>"
LOG.debug(
f"[default_handler] PRINTING output call #{default_handler._call_count}: "
f"{text_repr}"
)

print(text)
# Ensure stdout is flushed to prevent buffering
sys.stdout.flush()

handle_output = handle_output or default_handler
self.__output_thread = threading.Thread(
target=self._read_stdstreams, args=(handle_output,)
)
self.__output_thread.daemon = True
LOG.debug("[start_output] Starting daemon thread for output reading")
self.__output_thread.start()
LOG.debug("[start_output] Output thread started")

def end_output(self):
"""Stop streaming standard streams."""
LOG.debug("[end_output] Called to stop output streaming")
self.__ostream_service.end_streaming()

def _read_stdstreams(self, handle_output):
output_iter = self.__ostream_service.begin_streaming()
import time

LOG.debug("[_read_stdstreams] ENTER: Starting output streaming thread")
stream_start = time.time()
try:
LOG.debug("[_read_stdstreams] Calling begin_streaming()...")
output_iter = self.__ostream_service.begin_streaming()
LOG.debug(
"[_read_stdstreams] begin_streaming() returned, starting read loop"
)
except Exception as e:
LOG.error(
f"[_read_stdstreams] EXCEPTION calling begin_streaming: "
f"{type(e).__name__}: {e}"
)
return

text = ""
while True:
try:
response = next(output_iter)
text += response.text
if text and text[-1] == "\n":
handle_output(text[0:-1])
text = ""
except StopIteration:
# Flush any trailing text
if text:
handle_output(text)
break
chunk_count = 0
lines_flushed = 0
try:
while True:
try:
response = next(output_iter)
chunk_count += 1

# Extract text from response and get details
chunk_text = (
response.text if hasattr(response, "text") else str(response)
)
chunk_len = len(chunk_text)
has_newline = "\n" in chunk_text
is_whitespace_only = chunk_text.isspace() if chunk_len > 0 else True
text += chunk_text

# Log first 5 and every 100th chunk with detailed info
if chunk_count <= 5 or chunk_count % 100 == 0:
elapsed = time.time() - stream_start
if chunk_len == 0:
chunk_repr = "<empty>"
elif is_whitespace_only:
chunk_repr = f"<whitespace_only, {repr(chunk_text)}>"
else:
chunk_repr = repr(chunk_text[:60])
LOG.debug(
f"[_read_stdstreams] chunk {chunk_count} after {elapsed:.1f}s: "
f"len={chunk_len}, newline={has_newline}, accum_len={len(text)}, "
f"first60={chunk_repr}"
)

# Periodic progress log (every 50 chunks)
if chunk_count % 50 == 0:
elapsed = time.time() - stream_start
LOG.debug(
f"[_read_stdstreams] {chunk_count} chunks after {elapsed:.1f}s, "
f"lines={lines_flushed}, accum={len(text)}"
)

# Split and flush complete lines
while "\n" in text:
line, text = text.split("\n", 1)
lines_flushed += 1
# Log first 5 flushed lines and every 100th line
if lines_flushed <= 5 or lines_flushed % 100 == 0:
line_repr = repr(line[:80]) if len(line) > 0 else "<empty>"
LOG.debug(
f"[_read_stdstreams] FLUSHED line {lines_flushed}: {line_repr}"
)
handle_output(line)

except StopIteration:
elapsed = time.time() - stream_start
LOG.debug(
f"[_read_stdstreams] Stream ended after {chunk_count} chunks, "
f"{elapsed:.1f}s, lines_flushed={lines_flushed}"
)
# Flush any trailing text
if text:
LOG.debug(
f"[_read_stdstreams] Flushing trailing text: "
f"len={len(text)}, content={repr(text[:100])}"
)
handle_output(text)
lines_flushed += 1
break
except Exception as e:
elapsed = time.time() - stream_start
LOG.error(
f"[_read_stdstreams] EXCEPTION after {chunk_count} chunks, {elapsed:.1f}s: "
f"{type(e).__name__}: {e}"
)
break
finally:
elapsed = time.time() - stream_start
LOG.debug(
f"[_read_stdstreams] EXIT: {chunk_count} chunks, {lines_flushed} lines, "
f"{elapsed:.1f}s total"
)

def __getattr__(self, name):
"""Support command and query interfaces as method attributes, mainly to provide an
Expand Down
4 changes: 4 additions & 0 deletions src/ansys/systemcoupling/core/client/grpc_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

from ansys.systemcoupling.core.client.syc_launch_script import path_to_system_coupling
from ansys.systemcoupling.core.syc_version import (
SYC_LATEST_VERSION_CONCAT,
SYC_VERSION_CONCAT,
normalize_version,
)
Expand Down Expand Up @@ -153,6 +154,9 @@ def __init__(self, launching: bool, connection_type: ConnectionType, **kwargs):
version_str = SYC_VERSION_CONCAT

else:
# Might be connecting to a container in which case "latest" is a valid version string.
# We assume that this corresponds to a particular version of System Coupling.
version = SYC_LATEST_VERSION_CONCAT if version == "latest" else version
version_str = version if version else SYC_VERSION_CONCAT

# Store normalised version info
Expand Down
22 changes: 21 additions & 1 deletion src/ansys/systemcoupling/core/client/syc_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ def start_container(
else:
args = ["-m", "cosimgui", f"--grpcport=0.0.0.0:{port}", "--ptrace"]

# Apply server-side logging level if requested.
# PYSYC_SERVER_LOGGING_LEVEL is already handled in launch() for the process
# launch path, but in container mode the extra_args are not forwarded, so
# we check the env var here directly.
server_logging_level = os.getenv("PYSYC_SERVER_LOGGING_LEVEL")
if server_logging_level:
args = args + ["-l", server_logging_level]
LOG.info(
f"System Coupling container server logging level: {server_logging_level}"
)

LOG.debug("Starting System Coupling docker container...")

mounted_from = str(Path(mounted_from).absolute())
Expand All @@ -116,7 +127,6 @@ def start_container(
"docker",
"run",
"-d",
"--rm",
"-p",
f"{port}:{port}",
"-v",
Expand All @@ -130,6 +140,16 @@ def start_container(
f"ghcr.io/ansys/pysystem-coupling:{image_tag}",
] + args

# Optionally preserve container after exit for debugging (e.g., log extraction)
# By default, use --rm to clean up. Set PYSYC_PRESERVE_CONTAINER=1 to keep it.
preserve_container = os.getenv("PYSYC_PRESERVE_CONTAINER", "").lower() in (
"1",
"true",
"yes",
)
if not preserve_container:
run_args.insert(3, "--rm")

# Additional environment
container_user = os.getenv("SYC_CONTAINER_USER")
if container_user:
Expand Down
Loading
Loading