Skip to content

Commit f99a889

Browse files
Copilotakrherz
authored andcommitted
fix(pubsub): always update lastPublished when same item is overwritten (XEP-0060 §7.1.2)
Root cause of PubSubExtIntegrationTest failure: setLastPublishedItem() only updated the in-memory lastPublished cache when the new item's creation date was *strictly after* the existing one. When a publisher re-publishes an item with the same ItemID in rapid succession (within the same millisecond, common in integration tests), both items share the same CacheFactory.getClusterTime() value. The after() check returns false, so lastPublished is NOT updated. The persistence layer correctly performs an SQL UPDATE, but getPublishedItem() short-circuits by returning the stale in-memory lastPublished instead of going to the database, causing the test assertion ("item equals first, not second") to fail. Fix: add a third update condition — always update when the incoming item has the same unique identifier (same node + same ItemID) as the current lastPublished. Overwrites are always reflected in the cache regardless of timestamp resolution. Adds 3 regression tests in LeafNodeTest covering same-ID/same-time overwrite, same-time different-ID non-overwrite, and newer-time different-ID update. Co-authored-by: akrherz <210858+akrherz@users.noreply.github.com> (cherry picked from commit ddfc525)
1 parent d0a5052 commit f99a889

2 files changed

Lines changed: 114 additions & 2 deletions

File tree

xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/LeafNode.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,17 @@ protected void deletingNode() {
184184

185185
public synchronized void setLastPublishedItem(PublishedItem item)
186186
{
187-
if ((lastPublished == null) || (item != null) && item.getCreationDate().after(lastPublished.getCreationDate())) {
187+
// Always update when:
188+
// 1. There is no last-published item yet (initial state).
189+
// 2. The incoming item overwrites the current last-published item (same unique identifier).
190+
// XEP-0060 §7.1.2 requires the server to replace an existing item with the same ID.
191+
// Even if both items share the same creation-date (e.g. published in the same millisecond),
192+
// the in-memory cache must reflect the new payload so that getPublishedItem() does not
193+
// serve the stale first item.
194+
// 3. The incoming item is strictly newer than the current last-published item.
195+
if (item != null && (lastPublished == null
196+
|| lastPublished.getUniqueIdentifier().equals(item.getUniqueIdentifier())
197+
|| item.getCreationDate().after(lastPublished.getCreationDate()))) {
188198
Log.trace("Set last published item to: {}", item.getID());
189199
lastPublished = item;
190200
}

xmppserver/src/test/java/org/jivesoftware/openfire/pubsub/LeafNodeTest.java

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2020-2023 Ignite Realtime Foundation. All rights reserved.
2+
* Copyright (C) 2020-2026 Ignite Realtime Foundation. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@
2020
import org.junit.jupiter.api.Test;
2121
import org.xmpp.packet.JID;
2222

23+
import java.util.Date;
24+
2325
import static org.junit.jupiter.api.Assertions.*;
2426

2527
/**
@@ -29,6 +31,17 @@
2931
*/
3032
public class LeafNodeTest
3133
{
34+
/** Creates a minimal LeafNode for use in tests. */
35+
private static LeafNode createTestLeafNode() {
36+
final DefaultNodeConfiguration config = new DefaultNodeConfiguration(true);
37+
return new LeafNode(
38+
new PubSubService.UniqueIdentifier("test-service-id"),
39+
null,
40+
"test-node-id",
41+
new JID("unit-test@example.org"),
42+
config);
43+
}
44+
3245
@Test
3346
public void testSerialization() throws Exception
3447
{
@@ -59,4 +72,93 @@ public void testSerialization() throws Exception
5972
assertTrue( result instanceof LeafNode );
6073
assertEquals( input, result );
6174
}
75+
76+
// =========================================================================
77+
// setLastPublishedItem / getPublishedItem — XEP-0060 §7.1.2 compliance
78+
//
79+
// When a publisher publishes an item with the same ItemID as a previously
80+
// published item, the server MUST overwrite the old item with the new one.
81+
// The in-memory 'lastPublished' cache must reflect the new item immediately,
82+
// even if both items share the same creation-date timestamp.
83+
// =========================================================================
84+
85+
/**
86+
* Regression test for XEP-0060 §7.1.2: publishing a second item with the same
87+
* ItemID must update the in-memory {@code lastPublished} reference, even when
88+
* the two items share the same creation-date (published within the same
89+
* millisecond, as is common in fast sequential integration tests).
90+
*
91+
* <p>Before the fix, {@code setLastPublishedItem} only updated {@code lastPublished}
92+
* when {@code item.getCreationDate().after(lastPublished.getCreationDate())} returned
93+
* {@code true}. If both items had the same timestamp the old item remained cached,
94+
* causing {@code getPublishedItem()} to serve the stale first-published payload.
95+
*/
96+
@Test
97+
public void testSetLastPublishedItem_SameIdSameTimestamp_UpdatesCache() {
98+
final LeafNode node = createTestLeafNode();
99+
final JID publisher = new JID("user@example.org");
100+
final Date sharedTimestamp = new Date(1_000_000L);
101+
102+
// First publish: item1 with itemID="shared-id", payload conceptually "payload-1".
103+
final PublishedItem item1 = new PublishedItem(node, publisher, "shared-id", sharedTimestamp);
104+
node.setLastPublishedItem(item1);
105+
106+
// Sanity: lastPublished should now be item1.
107+
assertSame(item1, node.getLastPublishedItem(),
108+
"After first publish, lastPublished must be item1");
109+
110+
// Second publish: item2 with SAME itemID and SAME timestamp (typical in fast tests).
111+
final PublishedItem item2 = new PublishedItem(node, publisher, "shared-id", sharedTimestamp);
112+
node.setLastPublishedItem(item2);
113+
114+
// Assert: lastPublished must be updated to item2 (the overwriting item).
115+
// Before the fix this assertion would fail because the creation-date guard
116+
// prevented the update when both items shared the same timestamp.
117+
assertSame(item2, node.getLastPublishedItem(),
118+
"XEP-0060 §7.1.2: re-publishing with the same ItemID must update lastPublished " +
119+
"even when both items have the same creation-date");
120+
}
121+
122+
/**
123+
* Complementary test: publishing an item with a different ItemID but the same
124+
* timestamp must NOT overwrite {@code lastPublished}. Only items with newer
125+
* timestamps (or the same ID) should replace it.
126+
*/
127+
@Test
128+
public void testSetLastPublishedItem_DifferentIdSameTimestamp_DoesNotOverwrite() {
129+
final LeafNode node = createTestLeafNode();
130+
final JID publisher = new JID("user@example.org");
131+
final Date sharedTimestamp = new Date(1_000_000L);
132+
133+
final PublishedItem item1 = new PublishedItem(node, publisher, "id-alpha", sharedTimestamp);
134+
node.setLastPublishedItem(item1);
135+
136+
// A different item with a different ID but the same timestamp.
137+
final PublishedItem item2 = new PublishedItem(node, publisher, "id-beta", sharedTimestamp);
138+
node.setLastPublishedItem(item2);
139+
140+
// item1 was the last published (by time) and has a different ID,
141+
// so it should remain as lastPublished (item2 is not newer).
142+
assertSame(item1, node.getLastPublishedItem(),
143+
"A different item published at the same timestamp must not displace lastPublished");
144+
}
145+
146+
/**
147+
* Sanity test: publishing a newer item (strictly later timestamp) with a
148+
* different ID must update {@code lastPublished} as before.
149+
*/
150+
@Test
151+
public void testSetLastPublishedItem_DifferentIdNewerTimestamp_UpdatesCache() {
152+
final LeafNode node = createTestLeafNode();
153+
final JID publisher = new JID("user@example.org");
154+
155+
final PublishedItem item1 = new PublishedItem(node, publisher, "id-alpha", new Date(1_000L));
156+
node.setLastPublishedItem(item1);
157+
158+
final PublishedItem item2 = new PublishedItem(node, publisher, "id-beta", new Date(2_000L));
159+
node.setLastPublishedItem(item2);
160+
161+
assertSame(item2, node.getLastPublishedItem(),
162+
"A newer item (later timestamp, different ID) must update lastPublished");
163+
}
62164
}

0 commit comments

Comments
 (0)