Skip to content

Commit 29d389e

Browse files
committed
fix: prevent subscription cycles when adding subscriptions
1 parent 51ae8f2 commit 29d389e

File tree

6 files changed

+127
-5
lines changed

6 files changed

+127
-5
lines changed

lib/dal/src/attribute/attributes.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ use super::{
2525
AttributePrototypeArgument,
2626
static_value::StaticArgumentValue,
2727
},
28-
value::subscription::ValueSubscription,
28+
value::{
29+
AttributeValueError,
30+
subscription::ValueSubscription,
31+
},
2932
};
3033
use crate::{
3134
AttributePrototype,
@@ -36,7 +39,10 @@ use crate::{
3639
Prop,
3740
PropKind,
3841
WsEvent,
39-
component::resource::ResourceData,
42+
component::{
43+
resource::ResourceData,
44+
subscription_graph::SubscriptionGraph,
45+
},
4046
func::intrinsics::IntrinsicFunc,
4147
prop::PropError,
4248
workspace_snapshot::node_weight::{
@@ -392,6 +398,13 @@ async fn update_attributes_inner(
392398
)
393399
.await?;
394400

401+
if SubscriptionGraph::new(ctx).await?.is_cyclic() {
402+
return Err(AttributeValueError::SubscriptionWouldCycle {
403+
subscriber_av_id: target_av_id,
404+
subscription: subscription.fmt_title(ctx).await,
405+
})?;
406+
}
407+
395408
// Build the after value for the audit log!
396409
after_value_source = match subscription.resolve(ctx).await? {
397410
Some(resolved_av_id) => {

lib/dal/src/attribute/value.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,11 @@ pub enum AttributeValueError {
289289
subscription: String,
290290
subscription_prop_kind: PropKind,
291291
},
292+
#[error("Subscription would create a cycle between components")]
293+
SubscriptionWouldCycle {
294+
subscriber_av_id: AttributeValueId,
295+
subscription: String,
296+
},
292297
#[error("transactions error: {0}")]
293298
Transactions(#[from] TransactionsError),
294299
#[error("try lock error: {0}")]
@@ -1929,9 +1934,13 @@ impl AttributeValue {
19291934

19301935
/// Set the source of this attribute value to one or more subscriptions.
19311936
///
1932-
/// This overwrites or overrides any existing value; if your intent is to append
1933-
/// subscriptions, you should first call AttributeValue::subscriptions() and append to that
1934-
/// list.
1937+
/// This overwrites or overrides any existing value; if your intent is to
1938+
/// append subscriptions, you should first call
1939+
/// AttributeValue::subscriptions() and append to that list.
1940+
///
1941+
/// Note that subscriptions can create cycles in the graph. Use the
1942+
/// `SubscriptionGraph` to construct a dep graph of all components via their
1943+
/// subscriptions to check if a subscription creates a cycle.
19351944
pub async fn set_to_subscription(
19361945
ctx: &DalContext,
19371946
subscriber_av_id: AttributeValueId,

lib/dal/src/component.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ pub mod properties;
188188
pub mod qualification;
189189
pub mod resource;
190190
pub mod socket;
191+
pub mod subscription_graph;
191192
pub mod suggestion;
192193

193194
#[remain::sorted]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//! Component subscription graph.
2+
//!
3+
//! Creates a dependency graph by walking value subscription edges, so that we
4+
//! can detect if the subscriptions would create a cycle
5+
//!
6+
use si_id::ComponentId;
7+
8+
use super::{
9+
Component,
10+
ComponentResult,
11+
};
12+
use crate::{
13+
AttributePrototype,
14+
AttributeValue,
15+
DalContext,
16+
attribute::prototype::argument::AttributePrototypeArgument,
17+
dependency_graph::DependencyGraph,
18+
};
19+
20+
pub struct SubscriptionGraph {
21+
inner: DependencyGraph<ComponentId>,
22+
}
23+
24+
impl SubscriptionGraph {
25+
pub async fn new(ctx: &DalContext) -> ComponentResult<Self> {
26+
let components = Component::list_ids(ctx).await?;
27+
let mut inner = DependencyGraph::new();
28+
29+
for component_id in components {
30+
for (_, apa_id) in Component::subscribers(ctx, component_id).await? {
31+
let prototype_id = AttributePrototypeArgument::prototype_id(ctx, apa_id).await?;
32+
for av_id in AttributePrototype::attribute_value_ids(ctx, prototype_id).await? {
33+
let subscriber_component_id = AttributeValue::component_id(ctx, av_id).await?;
34+
// Don't add self-subscriptions
35+
if subscriber_component_id == component_id {
36+
continue;
37+
}
38+
inner.id_depends_on(subscriber_component_id, component_id);
39+
}
40+
}
41+
}
42+
43+
Ok(Self { inner })
44+
}
45+
46+
pub fn is_cyclic(&self) -> bool {
47+
self.inner.is_cyclic()
48+
}
49+
}

lib/dal/src/dependency_graph.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,8 @@ impl<T: Copy + std::cmp::Ord + std::cmp::Eq + std::cmp::PartialEq> DependencyGra
140140
pub fn all_ids(&self) -> impl Iterator<Item = T> {
141141
self.graph.node_weights().copied()
142142
}
143+
144+
pub fn is_cyclic(&self) -> bool {
145+
petgraph::algo::is_cyclic_directed(&self.graph)
146+
}
143147
}

lib/dal/tests/integration_test/attribute_value/subscription.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use dal::{
33
AttributeValue,
44
Component,
55
DalContext,
6+
component::subscription_graph::SubscriptionGraph,
67
func::authoring::FuncAuthoringClient,
78
};
89
use dal_test::{
@@ -733,6 +734,49 @@ async fn remove_subscribed_component(ctx: &mut DalContext) -> Result<()> {
733734
Ok(())
734735
}
735736

737+
#[test]
738+
async fn subscription_cycles(ctx: &mut DalContext) -> Result<()> {
739+
create_testy_variant(ctx).await?;
740+
component::create(ctx, "testy", "subscriber").await?;
741+
component::create(ctx, "testy", "source").await?;
742+
component::create(ctx, "testy", "source_2").await?;
743+
744+
value::subscribe(
745+
ctx,
746+
("subscriber", "/domain/Value"),
747+
("source", "/domain/Value"),
748+
)
749+
.await?;
750+
751+
value::subscribe(
752+
ctx,
753+
("source", "/domain/Value"),
754+
("source_2", "/domain/Value"),
755+
)
756+
.await?;
757+
758+
value::subscribe(
759+
ctx,
760+
("source_2", "/domain/Value3"),
761+
("source_2", "/domain/Value4"),
762+
)
763+
.await?;
764+
765+
assert_eq!(false, SubscriptionGraph::new(ctx).await?.is_cyclic());
766+
767+
// Return to the first one, creating a cycle
768+
value::subscribe(
769+
ctx,
770+
("source_2", "/domain/Value2"),
771+
("subscriber", "/domain/Value2"),
772+
)
773+
.await?;
774+
775+
assert!(SubscriptionGraph::new(ctx).await?.is_cyclic());
776+
777+
Ok(())
778+
}
779+
736780
async fn create_testy_variant(ctx: &DalContext) -> Result<()> {
737781
// Make a variant with a Value prop
738782
variant::create(
@@ -744,6 +788,8 @@ async fn create_testy_variant(ctx: &DalContext) -> Result<()> {
744788
props: [
745789
{ name: "Value", kind: "string" },
746790
{ name: "Value2", kind: "string" },
791+
{ name: "Value3", kind: "string" },
792+
{ name: "Value4", kind: "string" },
747793
{ name: "Values", kind: "array",
748794
entry: { name: "ValuesItem", kind: "string" },
749795
},

0 commit comments

Comments
 (0)