@Timeout
  public void replicateUsers() throws IOException {

    logger.info("replicating users from MySQL to CouchDB");
    List<SecuredUser> users = userService.list();

    List<User> usersView = userDb.getUsersView().<User>createDocQuery().asDocs();
    Map<String, User> couchUsers = User.toMap(usersView);

    List<User> newOrModifiedUsers = new ArrayList<>();

    // Add new users to couchdb
    for (SecuredUser user : users) {
      String id = user.getId().toString();
      if (!couchUsers.containsKey(id)) {
        String mmsi = getMmsi(user);
        String name = getName(user);
        newOrModifiedUsers.add(new User(id, user.getUserName(), name, mmsi));
        logger.info("Adding user with id={} and name={}", user.getId(), name);
      } else {
        User couchUser = couchUsers.get(id);
        String mmsi = getMmsi(user);
        String name = getName(user);
        if (!Objects.equals(couchUser.getName(), name)
            || !Objects.equals(couchUser.getMmsi(), mmsi)
            || !Objects.equals(couchUser.getUserName(), user.getUserName())) {
          couchUser.setMmsi(mmsi);
          couchUser.setName(name);
          couchUser.setUserName(user.getUserName());
          logger.info("Updating user with id={} and name={}", id, name);
          newOrModifiedUsers.add(couchUser);
        }
      }
    }
    userDb.bulk(newOrModifiedUsers);

    // remove deleted users from couchdb
    Map<Long, SecuredUser> securedUsers =
        users.stream().collect(Collectors.toMap(SecuredUser::getId, u -> u));
    List<User> toRemove = new ArrayList<>();
    for (User user : couchUsers.values()) {
      if (!securedUsers.containsKey(Long.parseLong(user.getDocId()))) {
        toRemove.add(user);
        user.setDeleted();
        logger.info("Removing user with id={} and name={}", user.getDocId(), user.getName());
      }
    }

    userDb.bulk(toRemove);
  }