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
- Stop the existing stopwatch first (stopWork) before starting a new session
- Check whether a stopwatch already exists for the user/issue and update/resume it instead of creating one
- Catch ExplicitException in scripts/REST callers and treat it as 'already running'
- 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
- Check existing stopwatch state before toggling time tracking
- Disable duplicate start actions in the UI while a request is in flight
- Stop watches when finishing work sessions to keep state clean
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
- Another pull request already opened for this change
- Invalid working period: ${value}
- An active subscription is required for this feature
- Time tracking needs to be enabled for the project
- Issue schedule permission required to set own estimated time
AI-assisted analysis of theonedev/onedev@d44925c47c (2026-09-06).
Data as JSON: /api/errors/fb40fa4b334cc62f.
Report an issue: GitHub.