Skip to content

Commit e63b3f6

Browse files
committed
gw: invalidate cached port_attrs on app upgrade via compose_hash
Same instance_id with a different compose_hash means the app was upgraded in place (typical for KMS-provisioned CVMs that reuse their disk). Previously, a legacy-style re-registration (port_attrs=None) would preserve stale cached attrs across such upgrades because the gateway assumed instance_id ↔ compose_hash was stable. Track the compose_hash each cached port_attrs was learned against (taken directly from the attested AppInfo, not from client input). Mismatch clears the cache so the lazy Info() fetch runs again.
1 parent ff521c1 commit e63b3f6

7 files changed

Lines changed: 53 additions & 9 deletions

File tree

gateway/src/debug_service.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ impl DebugRpc for DebugRpcHandler {
3535
&request.app_id,
3636
&request.instance_id,
3737
&request.client_public_key,
38+
"",
3839
None,
3940
)
4041
}

gateway/src/kv/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ pub struct InstanceData {
6262
/// Info() on first connection and populate this lazily.
6363
#[serde(default)]
6464
pub port_attrs: Option<BTreeMap<u16, PortFlags>>,
65+
/// Hex-encoded compose_hash that `port_attrs` was learned against.
66+
/// When a re-registration presents a different compose_hash (app upgrade),
67+
/// the cache is invalidated and re-fetched lazily.
68+
#[serde(default)]
69+
pub port_attrs_hash: String,
6570
}
6671

6772
/// Gateway node status (stored separately for independent updates)

gateway/src/main_service.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -382,11 +382,15 @@ impl Proxy {
382382
///
383383
/// `port_attrs = None` means the CVM didn't report port attributes (legacy
384384
/// CVM). The gateway will lazily fetch them via Info() on first connection.
385+
///
386+
/// `compose_hash` is the attested compose_hash — used to invalidate any
387+
/// cached `port_attrs` when the app is upgraded.
385388
pub fn do_register_cvm(
386389
&self,
387390
app_id: &str,
388391
instance_id: &str,
389392
client_public_key: &str,
393+
compose_hash: &str,
390394
port_attrs: Option<BTreeMap<u16, PortFlags>>,
391395
) -> Result<RegisterCvmResponse> {
392396
let mut state = self.lock();
@@ -407,7 +411,13 @@ impl Proxy {
407411
bail!("[{instance_id}] client public key is empty");
408412
}
409413
let client_info = state
410-
.new_client_by_id(instance_id, app_id, client_public_key, port_attrs)
414+
.new_client_by_id(
415+
instance_id,
416+
app_id,
417+
client_public_key,
418+
compose_hash,
419+
port_attrs,
420+
)
411421
.context("failed to allocate IP address for client")?;
412422
if let Err(err) = state.reconfigure() {
413423
error!("failed to reconfigure: {err:?}");
@@ -454,6 +464,7 @@ fn build_state_from_kv_store(instances: BTreeMap<String, InstanceData>) -> Proxy
454464
.checked_add(Duration::from_secs(data.reg_time))
455465
.unwrap_or(UNIX_EPOCH),
456466
port_attrs: data.port_attrs,
467+
port_attrs_hash: data.port_attrs_hash,
457468
connections: Default::default(),
458469
};
459470
state.allocated_addresses.insert(data.ip);
@@ -748,6 +759,7 @@ fn reload_instances_from_kv_store(proxy: &Proxy, store: &KvStore) -> Result<()>
748759
.checked_add(Duration::from_secs(data.reg_time))
749760
.unwrap_or(UNIX_EPOCH),
750761
port_attrs: data.port_attrs.clone(),
762+
port_attrs_hash: data.port_attrs_hash.clone(),
751763
connections: Default::default(),
752764
};
753765

@@ -829,6 +841,7 @@ impl ProxyState {
829841
id: &str,
830842
app_id: &str,
831843
public_key: &str,
844+
compose_hash: &str,
832845
port_attrs: Option<BTreeMap<u16, PortFlags>>,
833846
) -> Result<InstanceInfo> {
834847
if id.is_empty() {
@@ -848,11 +861,20 @@ impl ProxyState {
848861
// Update reg_time so other nodes will pick up the change
849862
existing.reg_time = SystemTime::now();
850863
}
864+
// App upgrade detection: a different attested compose_hash invalidates
865+
// any cached port_attrs from the previous code.
866+
if existing.port_attrs_hash != compose_hash {
867+
info!(
868+
"compose_hash changed for instance {id} ({} -> {compose_hash}), \
869+
invalidating cached port_attrs",
870+
existing.port_attrs_hash
871+
);
872+
existing.port_attrs = None;
873+
existing.port_attrs_hash = compose_hash.to_string();
874+
}
851875
// Only override cached port_attrs when the caller actually reports
852-
// them. A `None` request (legacy CVM) means "I don't know" — don't
853-
// wipe attrs learned at an earlier registration or lazy fetch. Same
854-
// instance_id implies same compose_hash, so cached attrs can't be
855-
// stale relative to the real app-compose.
876+
// them. A `None` request (legacy CVM) means "I don't know" — let
877+
// the lazy fetch path run again.
856878
if port_attrs.is_some() {
857879
existing.port_attrs = port_attrs.clone();
858880
}
@@ -865,6 +887,7 @@ impl ProxyState {
865887
public_key: existing.public_key.clone(),
866888
reg_time: encode_ts(existing.reg_time),
867889
port_attrs: existing.port_attrs.clone(),
890+
port_attrs_hash: existing.port_attrs_hash.clone(),
868891
};
869892
if let Err(err) = self.kv_store.sync_instance(&existing.id, &data) {
870893
error!("failed to sync existing instance to KvStore: {err:?}");
@@ -884,6 +907,7 @@ impl ProxyState {
884907
public_key: public_key.to_string(),
885908
reg_time: SystemTime::now(),
886909
port_attrs,
910+
port_attrs_hash: compose_hash.to_string(),
887911
connections: Default::default(),
888912
};
889913
self.add_instance(host_info.clone());
@@ -921,6 +945,7 @@ impl ProxyState {
921945
public_key: info.public_key.clone(),
922946
reg_time: encode_ts(info.reg_time),
923947
port_attrs: Some(attrs),
948+
port_attrs_hash: info.port_attrs_hash.clone(),
924949
};
925950
if let Err(err) = self.kv_store.sync_instance(instance_id, &data) {
926951
error!("failed to sync updated port_attrs to KvStore: {err:?}");
@@ -935,6 +960,7 @@ impl ProxyState {
935960
public_key: info.public_key.clone(),
936961
reg_time: encode_ts(info.reg_time),
937962
port_attrs: info.port_attrs.clone(),
963+
port_attrs_hash: info.port_attrs_hash.clone(),
938964
};
939965
if let Err(err) = self.kv_store.sync_instance(&info.id, &data) {
940966
error!("failed to sync instance to KvStore: {err:?}");
@@ -1339,14 +1365,20 @@ impl GatewayRpc for RpcHandler {
13391365
.context("App authorization failed")?;
13401366
let app_id = hex::encode(&app_info.app_id);
13411367
let instance_id = hex::encode(&app_info.instance_id);
1368+
let compose_hash = hex::encode(&app_info.compose_hash);
13421369
let port_attrs = request.port_attrs.map(|list| {
13431370
list.attrs
13441371
.into_iter()
13451372
.map(|p| (p.port as u16, PortFlags { pp: p.pp }))
13461373
.collect::<BTreeMap<u16, PortFlags>>()
13471374
});
1348-
self.state
1349-
.do_register_cvm(&app_id, &instance_id, &request.client_public_key, port_attrs)
1375+
self.state.do_register_cvm(
1376+
&app_id,
1377+
&instance_id,
1378+
&request.client_public_key,
1379+
&compose_hash,
1380+
port_attrs,
1381+
)
13501382
}
13511383

13521384
async fn acme_info(self) -> Result<AcmeInfoResponse> {

gateway/src/main_service/snapshots/dstack_gateway__main_service__tests__config-2.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ InstanceInfo {
1313
tv_nsec: 0,
1414
},
1515
port_attrs: None,
16+
port_attrs_hash: "",
1617
connections: 0,
1718
}

gateway/src/main_service/snapshots/dstack_gateway__main_service__tests__config.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ InstanceInfo {
1313
tv_nsec: 0,
1414
},
1515
port_attrs: None,
16+
port_attrs_hash: "",
1617
connections: 0,
1718
}

gateway/src/main_service/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ async fn test_config() {
5555
let state = create_test_state().await;
5656
let mut info = state
5757
.lock()
58-
.new_client_by_id("test-id-0", "app-id-0", "test-pubkey-0", None)
58+
.new_client_by_id("test-id-0", "app-id-0", "test-pubkey-0", "", None)
5959
.unwrap();
6060

6161
info.reg_time = SystemTime::UNIX_EPOCH;
6262
insta::assert_debug_snapshot!(info);
6363
let mut info1 = state
6464
.lock()
65-
.new_client_by_id("test-id-1", "app-id-1", "test-pubkey-1", None)
65+
.new_client_by_id("test-id-1", "app-id-1", "test-pubkey-1", "", None)
6666
.unwrap();
6767
info1.reg_time = SystemTime::UNIX_EPOCH;
6868
insta::assert_debug_snapshot!(info1);

gateway/src/models.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ pub struct InstanceInfo {
6666
/// gateway will lazily populate via Info() on first proxied connection.
6767
#[serde(default)]
6868
pub port_attrs: Option<BTreeMap<u16, PortFlags>>,
69+
/// Hex-encoded compose_hash that `port_attrs` was learned against. The
70+
/// cache is invalidated when a new registration presents a different hash.
71+
#[serde(default)]
72+
pub port_attrs_hash: String,
6973
#[serde(skip)]
7074
pub connections: Arc<AtomicU64>,
7175
}

0 commit comments

Comments
 (0)