How do I fix a Roster broken by inclusion of a no-longer-existing user?

How do I fix a Roster broken by inclusion of a no-longer-existing user?
If a portal user's account no longer exists, the "Roster" subchannel in Academus Collaborative Groupware displays a "Channel failed to render" message, with the following stack trace accompanying in portal.log:


ERROR [uPortal thread pool worker #227] jasig.portal.[] Jan/09 08:39:47 
- net.unicon.sdk.catalog.CatalogException: User does not exist:  AndrewPetro
net.unicon.sdk.catalog.CatalogException: User does not exist:  AndrewPetro
       at net.unicon.portal.channels.roster.RosterChannel$1
	   .convertRow(RosterChannel.java:2004)
       at net.unicon.portal.channels.roster.RosterDbDataSource
	   .fetchData(RosterDbDataSource.java:128)
       at net.unicon.sdk.catalog.FLazyCatalog.elements(FLazyCatalog.java:139)
       at net.unicon.portal.channels.roster.RosterChannel
	   .doPageAction(RosterChannel.java:616)
       at net.unicon.portal.channels.roster.RosterChannel
	   .buildXML(RosterChannel.java:462)
       at net.unicon.portal.common.AcademusMultithreadedChannel
	   .renderCharacters(AcademusMultithreadedChannel.java:183)
       at org.jasig.portal.MultithreadedCharacterChannelAdapter
	   .renderCharacters(MultithreadedCharacterChannelAdapter.java:71)
       at org.jasig.portal.ChannelRenderer$Worker.run(ChannelRenderer.java:483)
       at org.jasig.portal.utils.threading.Worker.run(Worker.java:88)
Caused by: net.unicon.academus.domain.ItemNotFoundException: 
Can't find user with user_name=AndrewPetro
       at net.unicon.academus.domain.lms.UserFactory
	   ._getUser(UserFactory.java:483)
       at net.unicon.academus.domain.lms.UserFactory
	   .getUser(UserFactory.java:448)
       at net.unicon.portal.channels.roster.RosterChannel$1
	   .convertRow(RosterChannel.java:2001)
       ... 8 more
ERROR [uPortal thread pool worker #235] jasig.portal.[] Jan/09 08:40:06 
- SuperChannel::renderState() : failed to render channel: 
net.unicon.portal.channels$org.jasig.portal.InternalPortalException
       at org.jasig.portal.ChannelRenderer
	   .completeRendering(ChannelRenderer.java:330)
       at net.unicon.portal.util.ChannelRendererWrapper
	   .getResults(ChannelRendererWrapper.java:132)
       at net.unicon.portal.channels.SuperChannel
	   .renderBuffers(SuperChannel.java:564)
       at net.unicon.portal.channels.SuperChannel
	   .renderState(SuperChannel.java:253)
       at net.unicon.portal.channels.SuperChannel
	   .buildXML(SuperChannel.java:217)
       at net.unicon.portal.channels.LmsChannel
	   .buildXML(LmsChannel.java:305)
       at net.unicon.portal.common.AcademusMultithreadedChannel
	   .renderCharacters(AcademusMultithreadedChannel.java:183)
       at org.jasig.portal.MultithreadedCharacterChannelAdapter
	   .renderCharacters(MultithreadedCharacterChannelAdapter.java:71)
       at org.jasig.portal.ChannelRenderer$Worker.run(ChannelRenderer.java:483)
       at org.jasig.portal.utils.threading.Worker.run(Worker.java:88)

making it impossible to perform any further updates to that group's enrollments via the web GUI.

This can be solved by directly modifying the Academus database: delete any rows from the 'membership' table that reference the offending user in the 'user_name' column. (Do this carefully, after backing up your data.)

Alternatively, this issue can be addressed through the Academus user interface: create a local-to-the-portal user with a username that matches that of the removed user. This user should then inherit the removed user's enrollments, and an admin should be able to access the roster channel to unenroll them. Once this is done, the portal user may be deleted.

In general it is a best practice to remove a user from Academus via the administrative user interfaces before the user is removed from backing user attribute stores (such as your enterprise LDAP).

Credit is due to Mark Roedel of LeTourneau University for contributing substantively to this knowledge base article.