theonedev/onedev · info · ExplicitException

Work already started

Error message

Work already started

What it means

DefaultStopwatchService.startWork() manages per-user issue stopwatches (time tracking). If a stopwatch already exists for the user/issue, starting another would double-count work time, so an ExplicitException 'Work already started' is thrown instead of persisting a duplicate.

Source

Thrown at server-core/src/main/java/io/onedev/server/service/impl/DefaultStopwatchService.java:50

		var criteria = newCriteria();
		criteria.add(Restrictions.eq(PROP_USER, user));
		criteria.add(Restrictions.eq(PROP_ISSUE, issue));
		return find(criteria);
	}
	
	@Transactional
	@Override
	public Stopwatch startWork(User user, Issue issue) {
		var watch = find(user, issue);
		if (watch == null) {
			watch = new Stopwatch();
			watch.setUser(user);
			watch.setIssue(issue);
			watch.setDate(new Date());
			dao.persist(watch);
			return watch;
		} else {
			throw new ExplicitException("Work already started");
		}
	}

	@Transactional
	@Override
	public void stopWork(Stopwatch stopwatch) {
		int spentMinutes = (int) ((System.currentTimeMillis() - stopwatch.getDate().getTime()) / 60000);
		if (spentMinutes > 0) {
			var work = new IssueWork();
			work.setUser(stopwatch.getUser());
			work.setIssue(stopwatch.getIssue());
			work.setDate(stopwatch.getDate());
			work.setMinutes(spentMinutes);
			workService.createOrUpdate(work);
		}
		dao.remove(stopwatch);
	}
		

View on GitHub (pinned to d44925c47c)

Solutions

  1. Stop the existing stopwatch first (stopWork) before starting a new session
  2. Check whether a stopwatch already exists for the user/issue and update/resume it instead of creating one
  3. Catch ExplicitException in scripts/REST callers and treat it as 'already running'
  4. Refresh the issue page to resync the actual timer state before toggling

Example fix

// before
stopwatchService.startWork(user, issue); // throws if already running
// after
Stopwatch existing = stopwatchService.find(user, issue);
if (existing == null)
    stopwatchService.startWork(user, issue);
else
    logger.info("Timer already running since {}", existing.getDate());
Defensive patterns

Strategy: validation

Validate before calling

Stopwatch existing = stopwatchService.find(user, issue);
if (existing == null)
    stopwatchService.startWork(user, issue);
else
    logger.info("Work already in progress since {}", existing.getDate());

Try / catch

try {
    stopwatchService.startWork(user, issue);
} catch (ExplicitException e) {
    if (e.getMessage().equals("Work already started"))
        resumeOrStopExistingWatch(user, issue);
    else throw e;
}

Prevention

When it happens

Trigger: Calling startWork(user, issue) (or the UI 'start timer' action) when dao finds an existing Stopwatch for the same user and issue whose work has not been stopped.

Common situations: Clicking start twice due to slow UI response; resuming an issue's timer that was left running since yesterday; API scripts toggling time tracking without checking current state; two browser tabs with stale timer state.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of theonedev/onedev@d44925c47c (2026-09-06). Data as JSON: /api/errors/fb40fa4b334cc62f. Report an issue: GitHub.