Commit c2c1eda
authored
observability: phase 6.1 concurrency-safe observer (PR-A) (#22)
* observability: phase 6.1 concurrency-safe observer
Phase 6.0 left three architectural limitations: OTelObserver state
collided across concurrent invocations on a shared instance, OTel
attach tokens spanned event boundaries (LIFO violations under
interleaved fan-out, suppressed by try/except guards in round-7),
and LLM spans had no calling-node identity threaded through, so
parent resolution fell back to the OTel current-span context and
mis-attributed under concurrent fan-out + retry. Spec agent's ack
folded the three into one coherent refactor — they share the same
primitive ("parents come from the observer's internal maps within
a single event handler's scope") and split would ship intermediate
states that fail their own correctness story.
Three new ContextVars in observability/correlation.py carry the
calling-node identity: current_namespace_prefix (default ()),
current_fan_out_index (default None), current_attempt_index
(default 0). The engine sets the first two in the outer per-node
scope of _step_function_node / _step_subgraph_node /
_step_fan_out_node; attempt_index lives inside innermost so retry
middleware that re-enters the inner closure bumps the value per
attempt. _LlmEventState gains calling_* fields populated from the
ContextVars at dispatch time.
OTelObserver state is now outer-keyed by invocation_id, not
correlation_id. Resume preserves the cid across runs (per spec
§5.6 cross-run join), so a cid-keyed map would have the resumed
run's spans inherit the prior invocation's trace_id, violating
§5.1's "each invocation has its own trace_id". Caught during impl
by test_phase5_fixture_031_span_assertions. The cid is still set
as openarmature.correlation_id on every span — only the state-
scoping key changed.
Parent resolution moves entirely to internal maps. _OpenSpan no
longer carries an OTel context token; each event handler reads
the parent from per-invocation open_spans / subgraph_spans /
detached_roots and starts the span with
context=set_span_in_context(parent). The round-7 try/except
ValueError guards on cross-event detach are deleted (no tokens
cross events). The close-prior-correlation_id branch in
_handle_started is deleted (per-invocation scoping makes it
unreachable). "Concurrency model — NOT safe" warning and
cross-event attach-token notes come out of the class docstring.
LLM-span parent resolution uses calling-node identity directly:
_resolve_llm_parent looks up open_spans by the full _StackKey
(calling_namespace_prefix, calling_attempt_index,
calling_fan_out_index), then walks ancestors for subgraph dispatch
or detached roots, then falls back to the invocation span. Under
concurrent fan-out + retry the LLM span lands under exactly the
right calling node regardless of dispatch ordering or which
sibling instance's span happens to be on the OTel current-span
stack.
Tests added in tests/unit/test_observability_otel.py:
- shared-observer concurrent invocations (asyncio.gather × 5):
N distinct trace_ids and clean per-invocation span trees.
- concurrent fan-out (4 instances) each calling LLM: every LLM
span parents under its own calling instance, not a sibling.
- LIFO regression check under warnings.catch_warnings("error"):
the path that previously needed try/except guards stays quiet
under the new architecture rather than being suppressed.
- LLM call inside retried node (max_attempts=3, two failures +
one success): each LLM span parents under its own attempt's
span, not a hardcoded attempt 0.
387 tests pass (4 new). Pyright clean.
* otel: address PR #22 review
- Fix _fan_out_node_span_key hardcoding attempt_index=0. Fan-out
nodes wrapped with retry middleware open at the bumped
attempt_index, so the lookup missed under retry and the
detached-instance Link wasn't added to the §4.4 cross-trace
navigation. Replaced with _find_fan_out_node_span scan that
finds the in-flight fan-out span at prefix regardless of
attempt_index (only one such entry is open at a time).
- Update close_invocation docstring to be honest about the
invocation_id sourcing constraint (CompiledGraph.invoke doesn't
return it; the ContextVar is reset before control returns) and
recommend shutdown() as the typical-production lifecycle hook.
The deeper UX gap (long-lived observers without periodic
shutdown accumulate per-invocation residue) is tracked in
phase-6-1-conformance-fillin.md for follow-up.
- Fix stale "now-cid-scoped" wording in openai.py — observer
state is invocation_id-keyed in the merged code.1 parent 41908c5 commit c2c1eda
6 files changed
Lines changed: 1011 additions & 450 deletions
File tree
- src/openarmature
- graph
- llm/providers
- observability
- otel
- tests/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
47 | 48 | | |
| 49 | + | |
48 | 50 | | |
| 51 | + | |
49 | 52 | | |
50 | 53 | | |
| 54 | + | |
51 | 55 | | |
| 56 | + | |
52 | 57 | | |
| 58 | + | |
53 | 59 | | |
54 | 60 | | |
55 | 61 | | |
| |||
558 | 564 | | |
559 | 565 | | |
560 | 566 | | |
561 | | - | |
562 | | - | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
563 | 572 | | |
564 | | - | |
565 | | - | |
566 | | - | |
567 | | - | |
568 | | - | |
569 | | - | |
570 | | - | |
571 | | - | |
572 | | - | |
573 | | - | |
574 | | - | |
575 | | - | |
576 | | - | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
577 | 603 | | |
578 | | - | |
579 | | - | |
580 | | - | |
581 | 604 | | |
582 | 605 | | |
583 | 606 | | |
584 | 607 | | |
585 | 608 | | |
586 | 609 | | |
587 | | - | |
| 610 | + | |
588 | 611 | | |
589 | 612 | | |
590 | | - | |
591 | | - | |
592 | | - | |
593 | | - | |
594 | | - | |
595 | | - | |
596 | | - | |
597 | | - | |
598 | | - | |
599 | | - | |
600 | | - | |
601 | | - | |
602 | | - | |
603 | | - | |
604 | | - | |
605 | | - | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
606 | 620 | | |
607 | 621 | | |
608 | 622 | | |
| |||
612 | 626 | | |
613 | 627 | | |
614 | 628 | | |
615 | | - | |
616 | | - | |
617 | | - | |
618 | | - | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
619 | 636 | | |
620 | 637 | | |
| 638 | + | |
| 639 | + | |
621 | 640 | | |
622 | 641 | | |
623 | 642 | | |
| |||
628 | 647 | | |
629 | 648 | | |
630 | 649 | | |
| 650 | + | |
| 651 | + | |
631 | 652 | | |
632 | 653 | | |
633 | 654 | | |
| |||
686 | 707 | | |
687 | 708 | | |
688 | 709 | | |
689 | | - | |
690 | | - | |
691 | | - | |
692 | | - | |
693 | | - | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
694 | 718 | | |
695 | 719 | | |
| 720 | + | |
| 721 | + | |
696 | 722 | | |
697 | 723 | | |
698 | 724 | | |
| |||
707 | 733 | | |
708 | 734 | | |
709 | 735 | | |
| 736 | + | |
| 737 | + | |
710 | 738 | | |
711 | 739 | | |
712 | 740 | | |
| |||
743 | 771 | | |
744 | 772 | | |
745 | 773 | | |
746 | | - | |
| 774 | + | |
747 | 775 | | |
748 | | - | |
749 | | - | |
750 | | - | |
751 | | - | |
752 | | - | |
753 | | - | |
754 | | - | |
755 | | - | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
756 | 805 | | |
757 | 806 | | |
758 | 807 | | |
759 | 808 | | |
760 | 809 | | |
761 | 810 | | |
762 | | - | |
| 811 | + | |
763 | 812 | | |
764 | 813 | | |
765 | | - | |
766 | | - | |
767 | | - | |
768 | | - | |
769 | | - | |
770 | | - | |
771 | | - | |
772 | | - | |
773 | | - | |
774 | | - | |
775 | | - | |
776 | | - | |
777 | | - | |
778 | | - | |
779 | | - | |
780 | | - | |
781 | | - | |
782 | | - | |
783 | | - | |
784 | | - | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
785 | 817 | | |
786 | 818 | | |
787 | 819 | | |
788 | 820 | | |
789 | 821 | | |
790 | 822 | | |
791 | 823 | | |
792 | | - | |
793 | | - | |
794 | | - | |
795 | | - | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
796 | 832 | | |
797 | 833 | | |
| 834 | + | |
| 835 | + | |
798 | 836 | | |
799 | 837 | | |
800 | 838 | | |
| |||
803 | 841 | | |
804 | 842 | | |
805 | 843 | | |
| 844 | + | |
| 845 | + | |
806 | 846 | | |
807 | 847 | | |
808 | 848 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
51 | 56 | | |
52 | 57 | | |
53 | 58 | | |
| |||
554 | 559 | | |
555 | 560 | | |
556 | 561 | | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
557 | 571 | | |
558 | 572 | | |
559 | 573 | | |
| |||
571 | 585 | | |
572 | 586 | | |
573 | 587 | | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
574 | 596 | | |
575 | 597 | | |
576 | 598 | | |
| |||
611 | 633 | | |
612 | 634 | | |
613 | 635 | | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
614 | 639 | | |
615 | 640 | | |
616 | 641 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
25 | 26 | | |
26 | 27 | | |
| 28 | + | |
27 | 29 | | |
| 30 | + | |
28 | 31 | | |
29 | 32 | | |
30 | 33 | | |
31 | 34 | | |
| 35 | + | |
32 | 36 | | |
33 | 37 | | |
| 38 | + | |
34 | 39 | | |
| 40 | + | |
35 | 41 | | |
0 commit comments