Modifying page content in a plugin fails with 7.15

Hi,

our plugin has to modify the page content to insert/update macros. The code in question (which no doubt has been written according to older guidelines) looks more or less like this:

Page page = pageManager.getPage(pageId);
Page originalPage = (Page) page.clone();
String content = page.getBodyAsString();
//modify content
page.setBodyAsString(content);
pageManager.saveContentEntity(page, originalPage, null);

which worked fine until 7.15. This all happens in a servlet.

In 7.15, when synchrony is also enabled (we don’t see the problem without synchrony), the hibernation layer dies horribly (optimistic locking exception etc.) thus making save functionality impossible. To make life more fun, the problem only occurs in production environments, not in development instances, so debugging this is close to impossible anyway.

So… we are aware that said code is deprecated anyway, but how should we change it? Is there any working sample out there?

We tried to wrap it into a HibernationTemplate, but couldn’t get Spring to actually load the plugin then, also we tried to rewrite it using ContentService, but we are unsure whether this is the right approach at all.

From customer reports, this seems to affect other plugins as well, and I’d say it’s a severe and uncommunicated incompatible change.

TIA

6 Likes

To answer myself:

We are also running in the issue mentioned here

So we can retrieve the old content and modify it just fine, but we aren’t able to actually update the content with ContentService.update()

I can’t believe there is NO working sample or even samplecode that shows how to reliably modify page content via either the old or the new Java API - seems that’s just something plugin developers aren’t supposed to do.

We are also having similar problems.
https://productsupport.adaptavist.com/browse/SRCONF-2082

We too have yet to find a workaround though typical hibernate methods (like wrapping things in a transaction template).

As @jcarter said, we are also experiencing an incredible number of Hibernate-related issues in version 7.15.0. Similar to you, we have code which modifies pages and simply uses the PageManager to save the entity. When doing so, we’re met with the same errors you’ve described - OptimisticLockingException, StaleObjectException, etc.

We tried to wrap it into a HibernationTemplate, but couldn’t get Spring to actually load the plugin then

We also went down this path, but again ran into similar problems as you have stated here. We are able to get ahold of com.atlassian.sal.api.transaction.TransactionTemplate, but that doesn’t seem to do anything for this particular case. Using com.atlassian.confluence.core.SynchronizationManager helps some, but doesn’t appear to be a consistent and real solution.

In 7.15, when synchrony is also enabled (we don’t see the problem without synchrony)

This is the same determining factor that we have also noted. With Collaborative Editing turned on (and thus Synchrony turned on), we run into a significant number of errors when modifying pages. We don’t seem to see this in other versions Confluence (it appears to work correctly on 7.4.11, atleast).

2 Likes

@JasmineMller and me already put some time into this but to no avail, unfortunately. Eventually, we’ve also managed wrapping it in a TransactionTemplate, but this didn’t make a difference for us.

Then, we tried to switch to the contentService approach, which is the suggested replacement for the deprecated pageManager stuff, although still being @ExperimentalAPI.
After hours of research (thanks to no documentation…), we came up with this:

      Content pageContent = contentService.find(new Expansion(Content.Expansions.BODY, new Expansions(new Expansion("storage"))),
              new Expansion(Content.Expansions.SPACE),
              new Expansion(Content.Expansions.VERSION)).withType(ContentType.PAGE).withId(ContentId.deserialise(pageId)).fetchOrNull();

       ContentBody body = pageContent.getBody().get(ContentRepresentation.STORAGE);

       Version nextVersion = pageContent
                .getVersion()
                .nextBuilder()
                .message("Message to explain the change")
                .by(User.fromUserkey(AuthenticatedUserThreadLocal.get().getKey()))
                .when(new Date())
                .content(body.getContentRef())
                .minorEdit(false)
                .hidden(false)
                .build();

        Content newContent = Content
                .builder(pageContent) // clone content of original page
                .version(nextVersion)
                .body(body.getValue(), body.getRepresentation()) // just rewrite the current body w/o change...
                .build();

        ContentService.Validator validator = contentService.validator();
        ValidationResult validationResult = validator.validateCreate(newContent);

        if (validationResult.isSuccessful()) {
          // Checks for both: validity and authorization.
          contentService.update(newContent);
        }

Unfortunately, the update call throws (running in a dev instance):

com.atlassian.confluence.api.service.exceptions.ServiceException: java.lang.IllegalStateException: Request-local WebResourceAssembler has already been initialised
	at com.atlassian.confluence.api.impl.service.content.ContentBodyConversionManagerImpl.lambda$computeConversionResources$1(ContentBodyConversionManagerImpl.java:110)
	at com.atlassian.confluence.api.impl.ReadOnlyAndReadWriteTransactionConversionTemplate.executeInReadWrite(ReadOnlyAndReadWriteTransactionConversionTemplate.java:65)
	at com.atlassian.confluence.api.impl.service.content.ContentBodyConversionManagerImpl.computeConversionResources(ContentBodyConversionManagerImpl.java:97)
	at com.atlassian.confluence.api.impl.service.content.ContentBodyConversionManagerImpl.convert(ContentBodyConversionManagerImpl.java:90)
...

The approach seemed promising to us, but the exception isn’t helpful at all.

After reducing the code as much as possible we noticed that the initial code snippet

Page page = pageManager.getPage(pageId);
Page originalPage = (Page) page.clone();
String content = page.getBodyAsString();
//modify content
page.setBodyAsString(content);
pageManager.saveContentEntity(page, originalPage, null);

actually works in Confluence 7.15 when not executed with other database interactions.

In our case, immediately before executing the page update with the pageManager we used the attachmentManager to update/save an associated attachment on the same page.

Commenting out the attachmentManager call, made the pageManager working properly (though I assume that other external concurrent database actions could still interfere…).

Therefore, we wrapped both of our database write accesses in a TransactionTemplate and this seems to work in our case in Confluence 7.15

transactionTemplate.execute(() -> {
  // [...]
  attachmentManager.saveAttachment(...);
  // [...]
  Page page = pageManager.getPage(pageId);
  Page originalPage = (Page) page.clone();
  String content = page.getBodyAsString();
  //modify content
  page.setBodyAsString(content);
  pageManager.saveContentEntity(page, originalPage, null);
  // [...]
  return null;
});
4 Likes

Thanks for the code @fabian.schwarzkopf ! I can also confirm that this appears to solve the problem with our scripts as well. :smile:

The problem we still have is that we still have a lot of testing infrastructure that would need to have similar changes made to it, but we are questioning whether this is necessary. It seems like the solution should be implemented on Atlassian’s side, as this only seems to be a recent problem made by changes in the Confluence source. I would like to know whether Atlassian intended for this to occur and/or if they have a more thorough explanation of what the resolution should be.

Thank you all for raising this and for contributing to this thread, we can confirm that the solution suggested by @fabian.schwarzkopf is the correct one. Please keep reading for more details on the issue itself, our investigation and the approach to address it.

Background & changes in Confluence

Atlassian app developers started noticing “ org.hibernate.StaleObjectStateException “ in our mutual customer logs using recent versions of Confluence.

Atlassian investigated the changes in the recent Confluence versions and concluded that the fix for the high-impact bug report caused the behaviour to start to manifest: CONFSERVER-55928.

However, we believe that the issue is not with the Confluence code and has to be be addressed by partners.

See below for more details on the issue itself and the Atlassian validated solution to prevent it in customer installations.

Understand the Issue

The partners report that something similar to this code works for them before.

// [...]
  attachmentManager.saveAttachment(...);
  // [...]
  Page page = pageManager.getPage(pageId); 
  Page originalPage = (Page) page.clone();
  String content = page.getBodyAsString();
  //modify content
  page.setBodyAsString(content);
  pageManager.saveContentEntity(page, originalPage, null); 

The reason it works without specify outer transaction is that Confluence automatically provides transaction wrapper for mostly all manager layer method call. So the code above will become:

  // [...]
  attachmentManager.saveAttachment(...); // ===> Transaction 1
  // [...]
  Page page = pageManager.getPage(pageId); // ===>  Transaction 2
  Page originalPage = (Page) page.clone();
  String content = page.getBodyAsString();
  //modify content
  page.setBodyAsString(content);
  pageManager.saveContentEntity(page, originalPage, null); // ===>  Transaction 3

And it works because there no relation between the call.

Now with the change from Unknowns Attachment Fix (CONFSERVER-55928). When Collaborative Editing is enabled, Confluence will try to trigger content reconciliation after attachment change. And this happen right after current transaction is completed. With the help from this code:

 synchronizationManager.runOnSuccessfulCommit(() -> {
      TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager, new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRES_NEW));
      transactionTemplate.execute(status -> {
          eventPublisher.publish(createContentUpdatedEvent(content, updateTrigger));
          return null;
      });
  });

Apply to the code above, mean that right after “Transaction 1“ the page will be updated. Then page object from line number 4 is stale. That is why vendor got org.hibernate.StaleObjectStateException whenever they try to update that object.

How to address it in the app

To address the issue above, please update the app code in the following way:

transactionTemplate.execute(() -> { // outter Transaction 
  // [...]
  attachmentManager.saveAttachment(...); // outter Transaction
  // [...]
  Page page = pageManager.getPage(pageId); // outter Transaction
  Page originalPage = (Page) page.clone();
  String content = page.getBodyAsString();
  //modify content
  page.setBodyAsString(content);
  pageManager.saveContentEntity(page, originalPage, null); // outter Transaction
  // [...]
  return null;
}); // page won't be updated by Reconciliation process until this line 

By wrapping all the code within one transaction, we makes sure all manager method call using same Transaction (as the default propagation level is PROPAGATION_REQUIRED ). And the stage of Hibernate object ( page` in this case) will consistent across method call.

Because the return value of manager’s method call could be a Hibernate object. From the best practice to work with Hibernate object is all of them must belong to single Unit of Work (section 11.1.1). Confluence provides transaction wrapper in manager’s method call to make it easier to use our API but partners should change their implementation to make it work more resilient and follow the best practices. More information of how to work with transaction in Confluence could be found in here

7 Likes