From 884e73d80752e779949284176173028f5feb0100 Mon Sep 17 00:00:00 2001
From: Eric Hartmann <hartmann.eric@gmail.com>
Date: Mon, 19 Jun 2017 16:41:19 +0200
Subject: MMF-935 experiment ES resilience of user creation

---
 .../authentication/UserIdentityAuthenticator.java  |  39 +-
 .../server/component/index/ComponentIndexer.java   |   2 +-
 .../main/java/org/sonar/server/es/BaseIndexer.java |  91 ----
 .../main/java/org/sonar/server/es/BulkIndexer.java |  66 ++-
 .../java/org/sonar/server/es/RecoveryIndexer.java  | 161 +++++++
 .../java/org/sonar/server/es/StartupIndexer.java   |   4 +-
 .../org/sonar/server/es/queue/package-info.java    |  23 +
 .../org/sonar/server/issue/index/IssueIndexer.java |   2 +-
 .../measure/index/ProjectMeasuresIndexer.java      |   2 +-
 .../organization/OrganizationCreationImpl.java     |   6 +-
 .../server/organization/ws/AddMemberAction.java    |   3 +-
 .../sonar/server/organization/ws/DeleteAction.java |   6 +-
 .../server/organization/ws/RemoveMemberAction.java |   3 +-
 .../server/permission/index/PermissionIndexer.java |   4 +-
 .../permission/index/PermissionIndexerDao.java     |   4 +-
 .../platform/platformlevel/PlatformLevel4.java     |   5 +-
 .../sonar/server/property/InternalProperties.java  |   2 +
 .../qualityprofile/index/ActiveRuleIndexer.java    |   2 +-
 .../org/sonar/server/test/index/TestIndexer.java   |   2 +-
 .../java/org/sonar/server/user/UserUpdater.java    |  77 ++--
 .../java/org/sonar/server/user/index/UserDoc.java  |  19 -
 .../server/user/index/UserIndexDefinition.java     |   4 -
 .../org/sonar/server/user/index/UserIndexer.java   | 122 ++++--
 .../server/user/index/UserResultSetIterator.java   |  53 +--
 .../sonar/server/user/ws/ChangePasswordAction.java |   2 +-
 .../org/sonar/server/user/ws/CreateAction.java     |   5 +-
 .../org/sonar/server/user/ws/DeactivateAction.java |   5 +-
 .../user/ws/SkipOnboardingTutorialAction.java      |   7 +-
 .../org/sonar/server/user/ws/UpdateAction.java     |   2 +-
 .../org/sonar/server/view/index/ViewIndexer.java   |   2 +-
 .../authentication/SsoAuthenticatorTest.java       |  11 +-
 .../UserIdentityAuthenticatorTest.java             |  41 +-
 .../java/org/sonar/server/es/BulkIndexerTest.java  |  48 +++
 .../org/sonar/server/es/RecoveryIndexerTest.java   | 394 +++++++++++++++++
 .../organization/OrganizationCreationImplTest.java |   7 +-
 .../server/organization/ws/CreateActionTest.java   |   3 +-
 .../server/organization/ws/DeleteActionTest.java   |   4 +-
 .../organization/ws/RemoveMemberActionTest.java    |   9 +-
 .../organization/ws/SearchMembersActionTest.java   |   6 +-
 .../sonar/server/user/ServerUserSessionTest.java   |   2 +-
 .../org/sonar/server/user/UserUpdaterTest.java     | 468 +++++++++++----------
 .../org/sonar/server/user/index/UserIndexTest.java |  10 +-
 .../sonar/server/user/index/UserIndexerTest.java   |  59 +--
 .../user/index/UserResultSetIteratorTest.java      | 101 -----
 .../server/user/ws/ChangePasswordActionTest.java   |  13 +-
 .../org/sonar/server/user/ws/CreateActionTest.java |   6 +-
 .../sonar/server/user/ws/DeactivateActionTest.java |   6 +-
 .../org/sonar/server/user/ws/SearchActionTest.java |   9 +-
 .../user/ws/SkipOnboardingTutorialActionTest.java  |  18 +-
 .../org/sonar/server/user/ws/UpdateActionTest.java |   5 +-
 50 files changed, 1202 insertions(+), 743 deletions(-)
 delete mode 100644 server/sonar-server/src/main/java/org/sonar/server/es/BaseIndexer.java
 create mode 100644 server/sonar-server/src/main/java/org/sonar/server/es/RecoveryIndexer.java
 create mode 100644 server/sonar-server/src/main/java/org/sonar/server/es/queue/package-info.java
 create mode 100644 server/sonar-server/src/test/java/org/sonar/server/es/RecoveryIndexerTest.java
 delete mode 100644 server/sonar-server/src/test/java/org/sonar/server/user/index/UserResultSetIteratorTest.java

(limited to 'server/sonar-server')

diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java
index 5b7ecb54220..396cea50b03 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java
@@ -86,21 +86,21 @@ public class UserIdentityAuthenticator {
     }
   }
 
-  private UserDto registerNewUser(DbSession dbSession, UserIdentity user, IdentityProvider provider, AuthenticationEvent.Source source) {
+  private UserDto registerNewUser(DbSession dbSession, UserIdentity identity, IdentityProvider provider, AuthenticationEvent.Source source) {
     if (!provider.allowsUsersToSignUp()) {
       throw AuthenticationException.newBuilder()
         .setSource(source)
-        .setLogin(user.getLogin())
+        .setLogin(identity.getLogin())
         .setMessage(format("User signup disabled for provider '%s'", provider.getKey()))
         .setPublicMessage(format("'%s' users are not allowed to sign up", provider.getKey()))
         .build();
     }
 
-    String email = user.getEmail();
+    String email = identity.getEmail();
     if (email != null && dbClient.userDao().doesEmailExist(dbSession, email)) {
       throw AuthenticationException.newBuilder()
         .setSource(source)
-        .setLogin(user.getLogin())
+        .setLogin(identity.getLogin())
         .setMessage(format("Email '%s' is already used", email))
         .setPublicMessage(format(
           "You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.",
@@ -108,25 +108,22 @@ public class UserIdentityAuthenticator {
         .build();
     }
 
-    String userLogin = user.getLogin();
-    userUpdater.create(dbSession, NewUser.builder()
+    String userLogin = identity.getLogin();
+    return userUpdater.createAndCommit(dbSession, NewUser.builder()
       .setLogin(userLogin)
-      .setEmail(user.getEmail())
-      .setName(user.getName())
-      .setExternalIdentity(new ExternalIdentity(provider.getKey(), user.getProviderLogin()))
-      .build());
-    UserDto newUser = dbClient.userDao().selectOrFailByLogin(dbSession, userLogin);
-    syncGroups(dbSession, user, newUser);
-    return newUser;
+      .setEmail(identity.getEmail())
+      .setName(identity.getName())
+      .setExternalIdentity(new ExternalIdentity(provider.getKey(), identity.getProviderLogin()))
+      .build(), u -> syncGroups(dbSession, identity, u));
   }
 
-  private void registerExistingUser(DbSession dbSession, UserDto userDto, UserIdentity user, IdentityProvider provider) {
-    userUpdater.update(dbSession, UpdateUser.create(userDto.getLogin())
-      .setEmail(user.getEmail())
-      .setName(user.getName())
-      .setExternalIdentity(new ExternalIdentity(provider.getKey(), user.getProviderLogin()))
-      .setPassword(null));
-    syncGroups(dbSession, user, userDto);
+  private void registerExistingUser(DbSession dbSession, UserDto userDto, UserIdentity identity, IdentityProvider provider) {
+    UpdateUser update = UpdateUser.create(userDto.getLogin())
+      .setEmail(identity.getEmail())
+      .setName(identity.getName())
+      .setExternalIdentity(new ExternalIdentity(provider.getKey(), identity.getProviderLogin()))
+      .setPassword(null);
+    userUpdater.updateAndCommit(dbSession, update, u -> syncGroups(dbSession, identity, u));
   }
 
   private void syncGroups(DbSession dbSession, UserIdentity userIdentity, UserDto userDto) {
@@ -149,8 +146,6 @@ public class UserIdentityAuthenticator {
 
     addGroups(dbSession, userDto, groupsToAdd, groupsByName);
     removeGroups(dbSession, userDto, groupsToRemove, groupsByName);
-
-    dbSession.commit();
   }
 
   private void addGroups(DbSession dbSession, UserDto userDto, Collection<String> groupsToAdd, Map<String, GroupDto> groupsByName) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java
index 429ae9dedf6..d845806e06b 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java
@@ -59,7 +59,7 @@ public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexe
   }
 
   @Override
-  public void indexOnStartup(Set<IndexType> emptyIndexTypes) {
+  public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
     doIndexByProjectUuid(null, Size.LARGE);
   }
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/BaseIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/es/BaseIndexer.java
deleted file mode 100644
index f83fa993468..00000000000
--- a/server/sonar-server/src/main/java/org/sonar/server/es/BaseIndexer.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.es;
-
-import com.google.common.base.Throwables;
-import com.google.common.util.concurrent.Uninterruptibles;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import org.picocontainer.Startable;
-import org.sonar.api.utils.System2;
-
-public abstract class BaseIndexer implements Startable {
-
-  private final System2 system2;
-  private final ThreadPoolExecutor executor;
-  private final IndexType indexType;
-  protected final EsClient esClient;
-  private final String dateFieldName;
-  private volatile long lastUpdatedAt = -1L;
-
-  protected BaseIndexer(System2 system2, EsClient client, long threadKeepAliveSeconds, IndexType indexType,
-    String dateFieldName) {
-    this.system2 = system2;
-    this.indexType = indexType;
-    this.dateFieldName = dateFieldName;
-    this.esClient = client;
-    this.executor = new ThreadPoolExecutor(0, 1,
-      threadKeepAliveSeconds, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
-  }
-
-  public void index(final IndexerTask task) {
-    final long requestedAt = system2.now();
-    Future submit = executor.submit(() -> {
-      if (lastUpdatedAt == -1L) {
-        lastUpdatedAt = esClient.getMaxFieldValue(indexType, dateFieldName);
-      }
-      if (requestedAt > lastUpdatedAt) {
-        long l = task.index(lastUpdatedAt);
-        // l can be 0 if no documents were indexed
-        lastUpdatedAt = Math.max(l, lastUpdatedAt);
-      }
-    });
-    try {
-      Uninterruptibles.getUninterruptibly(submit);
-    } catch (ExecutionException e) {
-      Throwables.propagate(e);
-    }
-  }
-
-  public void index() {
-    index(this::doIndex);
-  }
-
-  protected abstract long doIndex(long lastUpdatedAt);
-
-  @Override
-  public void start() {
-    // nothing to do at startup
-  }
-
-  @Override
-  public void stop() {
-    executor.shutdown();
-  }
-
-  @FunctionalInterface
-  public interface IndexerTask {
-    long index(long lastUpdatedAt);
-  }
-
-}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexer.java
index 3d5b5439495..8aaff572899 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexer.java
@@ -20,10 +20,15 @@
 package org.sonar.server.es;
 
 import com.google.common.annotations.VisibleForTesting;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
+import javax.annotation.Nullable;
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.admin.indices.settings.get.GetSettingsResponse;
 import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequestBuilder;
@@ -43,12 +48,15 @@ import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.search.SearchHitField;
 import org.elasticsearch.search.sort.SortOrder;
-import org.picocontainer.Startable;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.core.util.ProgressLogger;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
 
 import static java.lang.String.format;
+import static java.util.stream.Collectors.toList;
 
 /**
  * Helper to bulk requests in an efficient way :
@@ -57,7 +65,7 @@ import static java.lang.String.format;
  *   <li>on large table indexing, replicas and automatic refresh can be temporarily disabled</li>
  * </ul>
  */
-public class BulkIndexer implements Startable {
+public class BulkIndexer {
 
   private static final Logger LOGGER = Loggers.get(BulkIndexer.class);
   private static final ByteSizeValue FLUSH_BYTE_SIZE = new ByteSizeValue(1, ByteSizeUnit.MB);
@@ -69,13 +77,22 @@ public class BulkIndexer implements Startable {
   private final String indexName;
   private final BulkProcessor bulkProcessor;
   private final AtomicLong counter = new AtomicLong(0L);
+  private final AtomicLong successCounter = new AtomicLong(0L);
   private final SizeHandler sizeHandler;
+  private final BulkProcessorListener bulkProcessorListener;
+  @Nullable
+  private DbClient dbClient;
+  @Nullable
+  private DbSession dbSession;
+  private Collection<EsQueueDto> esQueueDtos;
 
   public BulkIndexer(EsClient client, String indexName, Size size) {
+    this.dbClient = null;
     this.client = client;
     this.indexName = indexName;
     this.sizeHandler = size.createHandler(Runtime2.INSTANCE);
-    this.bulkProcessor = BulkProcessor.builder(client.nativeClient(), new BulkProcessorListener())
+    this.bulkProcessorListener = new BulkProcessorListener();
+    this.bulkProcessor = BulkProcessor.builder(client.nativeClient(), bulkProcessorListener)
       .setBackoffPolicy(BackoffPolicy.exponentialBackoff())
       .setBulkSize(FLUSH_BYTE_SIZE)
       .setBulkActions(FLUSH_ACTIONS)
@@ -83,22 +100,36 @@ public class BulkIndexer implements Startable {
       .build();
   }
 
-  @Override
   public void start() {
     sizeHandler.beforeStart(this);
     counter.set(0L);
+    successCounter.set(0L);
   }
 
-  @Override
-  public void stop() {
+  public void start(DbSession dbSession, DbClient dbClient, Collection<EsQueueDto> esQueueDtos) {
+    this.dbClient = dbClient;
+    this.dbSession = dbSession;
+    this.esQueueDtos = esQueueDtos;
+    sizeHandler.beforeStart(this);
+    counter.set(0L);
+    successCounter.set(0L);
+  }
+
+  /**
+   * @return the number of documents successfully indexed
+   */
+  public long stop() {
     try {
-      bulkProcessor.awaitClose(10, TimeUnit.MINUTES);
+      bulkProcessor.awaitClose(1, TimeUnit.MINUTES);
     } catch (InterruptedException e) {
       Thread.currentThread().interrupt();
-      throw new IllegalStateException("Elasticsearch bulk requests still being executed after 10 minutes", e);
+      throw new IllegalStateException("Elasticsearch bulk requests still being executed after 1 minute", e);
+    } finally {
+      dbSession = null;
     }
     client.prepareRefresh(indexName).get();
     sizeHandler.afterStop(this);
+    return successCounter.get();
   }
 
   public void add(ActionRequest<?> request) {
@@ -161,7 +192,6 @@ public class BulkIndexer implements Startable {
   }
 
   private final class BulkProcessorListener implements Listener {
-
     @Override
     public void beforeBulk(long executionId, BulkRequest request) {
       // no action required
@@ -174,14 +204,31 @@ public class BulkIndexer implements Startable {
       for (BulkItemResponse item : response.getItems()) {
         if (item.isFailed()) {
           LOGGER.error("index [{}], type [{}], id [{}], message [{}]", item.getIndex(), item.getType(), item.getId(), item.getFailureMessage());
+        } else {
+          successCounter.incrementAndGet();
         }
       }
+
+      deleteSuccessfulItems(response);
     }
 
     @Override
     public void afterBulk(long executionId, BulkRequest req, Throwable e) {
       LOGGER.error("Fail to execute bulk index request: " + req, e);
     }
+
+    private void deleteSuccessfulItems(BulkResponse bulkResponse) {
+      if (esQueueDtos != null) {
+        List<EsQueueDto> itemsToDelete = Arrays.stream(bulkResponse.getItems())
+          .filter(b -> !b.isFailed())
+          .map(b -> esQueueDtos.stream().filter(t -> b.getId().equals(t.getDocUuid())).findFirst().orElse(null))
+          .filter(Objects::nonNull)
+          .collect(toList());
+
+        dbClient.esQueueDao().delete(dbSession, itemsToDelete);
+        dbSession.commit();
+      }
+    }
   }
 
   public enum Size {
@@ -293,5 +340,4 @@ public class BulkIndexer implements Startable {
       req.get();
     }
   }
-
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/RecoveryIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/es/RecoveryIndexer.java
new file mode 100644
index 00000000000..c7ffdb9b2d6
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/es/RecoveryIndexer.java
@@ -0,0 +1,161 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.es;
+
+import com.google.common.collect.ListMultimap;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang.math.RandomUtils;
+import org.sonar.api.Startable;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
+import org.sonar.server.user.index.UserIndexer;
+
+import static java.lang.String.format;
+
+public class RecoveryIndexer implements Startable {
+
+  private static final Logger LOGGER = Loggers.get(RecoveryIndexer.class);
+  private static final String LOG_PREFIX = "Elasticsearch recovery - ";
+  private static final String PROPERTY_INITIAL_DELAY = "sonar.search.recovery.initialDelayInMs";
+  private static final String PROPERTY_DELAY = "sonar.search.recovery.delayInMs";
+  private static final String PROPERTY_MIN_AGE = "sonar.search.recovery.minAgeInMs";
+  private static final String PROPERTY_LOOP_LIMIT = "sonar.search.recovery.loopLimit";
+  private static final long DEFAULT_DELAY_IN_MS = 5L * 60 * 1000;
+  private static final long DEFAULT_MIN_AGE_IN_MS = 5L * 60 * 1000;
+  private static final int DEFAULT_LOOP_LIMIT = 10_000;
+  private static final double CIRCUIT_BREAKER_IN_PERCENT = 0.3;
+
+  private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1,
+    new ThreadFactoryBuilder()
+      .setPriority(Thread.MIN_PRIORITY)
+      .setNameFormat("RecoveryIndexer-%d")
+      .build());
+  private final System2 system2;
+  private final Settings settings;
+  private final DbClient dbClient;
+  private final UserIndexer userIndexer;
+  private final long minAgeInMs;
+  private final long loopLimit;
+
+  public RecoveryIndexer(System2 system2, Settings settings, DbClient dbClient, UserIndexer userIndexer) {
+    this.system2 = system2;
+    this.settings = settings;
+    this.dbClient = dbClient;
+    this.userIndexer = userIndexer;
+    this.minAgeInMs = getSetting(PROPERTY_MIN_AGE, DEFAULT_MIN_AGE_IN_MS);
+    this.loopLimit = getSetting(PROPERTY_LOOP_LIMIT, DEFAULT_LOOP_LIMIT);
+  }
+
+  @Override
+  public void start() {
+    long delayInMs = getSetting(PROPERTY_DELAY, DEFAULT_DELAY_IN_MS);
+
+    // in the cluster mode, avoid (but not prevent!) simultaneous executions of recovery
+    // indexers so that a document is not handled multiple times.
+    long initialDelayInMs = getSetting(PROPERTY_INITIAL_DELAY, RandomUtils.nextInt(1 + (int) (delayInMs / 2)));
+
+    executorService.scheduleAtFixedRate(
+      this::recover,
+      initialDelayInMs,
+      delayInMs,
+      TimeUnit.MILLISECONDS);
+  }
+
+  @Override
+  public void stop() {
+    try {
+      executorService.shutdown();
+      executorService.awaitTermination(5, TimeUnit.SECONDS);
+    } catch (InterruptedException e) {
+      LOGGER.error(LOG_PREFIX + "Unable to stop recovery indexer in timely fashion", e);
+      executorService.shutdownNow();
+      Thread.currentThread().interrupt();
+    }
+  }
+
+  void recover() {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      Profiler profiler = Profiler.create(LOGGER).start();
+      long beforeDate = system2.now() - minAgeInMs;
+      long total = 0L;
+      long totalSuccess = 0L;
+
+      Collection<EsQueueDto> items = dbClient.esQueueDao().selectForRecovery(dbSession, beforeDate, loopLimit);
+      while (!items.isEmpty()) {
+        total += items.size();
+        long loopSuccess = 0L;
+
+        ListMultimap<EsQueueDto.Type, EsQueueDto> itemsByType = groupItemsByType(items);
+        for (Map.Entry<EsQueueDto.Type, Collection<EsQueueDto>> entry : itemsByType.asMap().entrySet()) {
+          loopSuccess += doIndex(dbSession, entry.getKey(), entry.getValue());
+        }
+
+        totalSuccess += loopSuccess;
+        if (1.0d * (items.size() - loopSuccess) / items.size() >= CIRCUIT_BREAKER_IN_PERCENT) {
+          LOGGER.error(LOG_PREFIX + "too many failures [{}/{} documents], waiting for next run", items.size() - loopSuccess, items.size());
+          break;
+        }
+        items = dbClient.esQueueDao().selectForRecovery(dbSession, beforeDate, loopLimit);
+      }
+      if (total > 0L) {
+        profiler.stopInfo(LOG_PREFIX + format("%d documents processed [%d failures]", total, total - totalSuccess));
+      }
+    } catch (Throwable t) {
+      LOGGER.error(LOG_PREFIX + "fail to recover documents", t);
+    }
+  }
+
+  private long doIndex(DbSession dbSession, EsQueueDto.Type type, Collection<EsQueueDto> typeItems) {
+    LOGGER.trace(LOG_PREFIX + "processing {} {}", typeItems.size(), type);
+    switch (type) {
+      case USER:
+        return userIndexer.index(dbSession, typeItems);
+      default:
+        LOGGER.error(LOG_PREFIX + "ignore {} documents with unsupported type {}", typeItems.size(), type);
+        return 0;
+    }
+  }
+
+  private static ListMultimap<EsQueueDto.Type, EsQueueDto> groupItemsByType(Collection<EsQueueDto> items) {
+    return items.stream().collect(MoreCollectors.index(EsQueueDto::getDocType));
+  }
+
+  private long getSetting(String key, long defaultValue) {
+    long val = settings.getLong(key);
+    if (val <= 0) {
+      val = defaultValue;
+    }
+    LOGGER.debug(LOG_PREFIX + "{}={}", key, val);
+    return val;
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/StartupIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/es/StartupIndexer.java
index d007e402713..a1fef5fabf6 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/es/StartupIndexer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/es/StartupIndexer.java
@@ -28,9 +28,9 @@ public interface StartupIndexer {
 
   /**
    * This reindexing method will only be called on startup, and only,
-   * if there is at least one empty types.
+   * if there is at least one uninitialized type.
    */
-  void indexOnStartup(Set<IndexType> emptyIndexTypes);
+  void indexOnStartup(Set<IndexType> uninitializedIndexTypes);
 
   Set<IndexType> getIndexTypes();
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/queue/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/es/queue/package-info.java
new file mode 100644
index 00000000000..89f951738d6
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/es/queue/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.es.queue;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java
index 4eaa6523a30..5dbf88356bb 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java
@@ -69,7 +69,7 @@ public class IssueIndexer implements ProjectIndexer, NeedAuthorizationIndexer, S
   }
 
   @Override
-  public void indexOnStartup(Set<IndexType> emptyIndexTypes) {
+  public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
     doIndex(createBulkIndexer(Size.LARGE), (String) null);
   }
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java
index e7851052f49..84d52030769 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java
@@ -59,7 +59,7 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization
   }
 
   @Override
-  public void indexOnStartup(Set<IndexType> emptyIndexTypes) {
+  public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
     doIndex(createBulkIndexer(Size.LARGE), (String) null);
   }
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java b/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java
index 25a4939746b..010d6dfafad 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java
@@ -106,11 +106,10 @@ public class OrganizationCreationImpl implements OrganizationCreation {
       addCurrentUserToGroup(dbSession, ownerGroup, userCreator.getId());
       addCurrentUserToGroup(dbSession, defaultGroup, userCreator.getId());
 
-      dbSession.commit();
       batchDbSession.commit();
 
       // Elasticsearch is updated when DB session is committed
-      userIndexer.index(userCreator.getLogin());
+      userIndexer.commitAndIndex(dbSession, userCreator);
 
       return organization;
     }
@@ -144,11 +143,10 @@ public class OrganizationCreationImpl implements OrganizationCreation {
       insertQualityProfiles(dbSession, batchDbSession, organization);
       addCurrentUserToGroup(dbSession, defaultGroup, newUser.getId());
 
-      dbSession.commit();
       batchDbSession.commit();
 
       // Elasticsearch is updated when DB session is committed
-      userIndexer.index(newUser.getLogin());
+      userIndexer.commitAndIndex(dbSession, newUser);
 
       return Optional.of(organization);
     }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/AddMemberAction.java b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/AddMemberAction.java
index 7c6c1242ded..535ace66812 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/AddMemberAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/AddMemberAction.java
@@ -118,8 +118,7 @@ public class AddMemberAction implements OrganizationsWsAction {
       .setUserId(user.getId()));
     dbClient.userGroupDao().insert(dbSession,
       new UserGroupDto().setGroupId(defaultGroupFinder.findDefaultGroup(dbSession, organization.getUuid()).getId()).setUserId(user.getId()));
-    dbSession.commit();
-    userIndexer.index(user.getLogin());
+    userIndexer.commitAndIndex(dbSession, user);
   }
 
   private AddMemberWsResponse buildResponse(UserDto user, int groups) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java
index bd99338c81f..f3c4f596446 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.organization.ws;
 
+import java.util.Collection;
 import java.util.List;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
@@ -138,11 +139,10 @@ public class DeleteAction implements OrganizationsWsAction {
   }
 
   private void deleteOrganization(DbSession dbSession, OrganizationDto organization) {
-    List<String> logins = dbClient.organizationMemberDao().selectLoginsByOrganizationUuid(dbSession, organization.getUuid());
+    Collection<String> logins = dbClient.organizationMemberDao().selectLoginsByOrganizationUuid(dbSession, organization.getUuid());
     dbClient.organizationMemberDao().deleteByOrganizationUuid(dbSession, organization.getUuid());
     dbClient.organizationDao().deleteByUuid(dbSession, organization.getUuid());
-    dbSession.commit();
-    userIndexer.index(logins);
+    userIndexer.commitAndIndexByLogins(dbSession, logins);
   }
 
   private static void preventDeletionOfDefaultOrganization(String key, DefaultOrganization defaultOrganization) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/RemoveMemberAction.java b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/RemoveMemberAction.java
index 5c81f8b17bb..a927bdfcbd5 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/RemoveMemberAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/RemoveMemberAction.java
@@ -103,8 +103,7 @@ public class RemoveMemberAction implements OrganizationsWsAction {
     dbClient.propertiesDao().deleteByOrganizationAndMatchingLogin(dbSession, organizationUuid, user.getLogin(), singletonList(DEFAULT_ISSUE_ASSIGNEE));
 
     dbClient.organizationMemberDao().delete(dbSession, organizationUuid, userId);
-    dbSession.commit();
-    userIndexer.index(user.getLogin());
+    userIndexer.commitAndIndex(dbSession, user);
   }
 
   private void ensureLastAdminIsNotRemoved(DbSession dbSession, OrganizationDto organizationDto, UserDto user) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java
index 41dd5b64ba4..8c7ad3e6693 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java
@@ -80,9 +80,9 @@ public class PermissionIndexer implements ProjectIndexer, StartupIndexer {
   }
 
   @Override
-  public void indexOnStartup(Set<IndexType> emptyIndexTypes) {
+  public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
     List<Dto> authorizations = getAllAuthorizations();
-    Stream<AuthorizationScope> scopes = getScopes(emptyIndexTypes);
+    Stream<AuthorizationScope> scopes = getScopes(uninitializedIndexTypes);
     index(authorizations, scopes, Size.LARGE);
   }
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java
index 81c98dc39c8..55d575a8269 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java
@@ -32,8 +32,8 @@ import org.apache.commons.lang.StringUtils;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 
+import static org.apache.commons.lang.StringUtils.repeat;
 import static org.sonar.db.DatabaseUtils.executeLargeInputs;
-import static org.sonar.db.DatabaseUtils.repeatCondition;
 
 /**
  * No streaming because of union of joins -> no need to use ResultSetIterator
@@ -200,7 +200,7 @@ public class PermissionIndexerDao {
     if (projectUuids.isEmpty()) {
       sql = StringUtils.replace(SQL_TEMPLATE, "{projectsCondition}", "");
     } else {
-      sql = StringUtils.replace(SQL_TEMPLATE, "{projectsCondition}", " AND (" + repeatCondition("projects.uuid = ?", projectUuids.size(), "OR") + ")");
+      sql = StringUtils.replace(SQL_TEMPLATE, "{projectsCondition}", " AND (" + repeat("projects.uuid = ?", " or ", projectUuids.size()) + ")");
     }
     PreparedStatement stmt = dbClient.getMyBatis().newScrollingSelectStatement(session, sql);
     int index = 1;
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index ac0aa79be26..434eab1d84e 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -53,6 +53,7 @@ import org.sonar.server.duplication.ws.ShowResponseBuilder;
 import org.sonar.server.email.ws.EmailsWsModule;
 import org.sonar.server.es.IndexCreator;
 import org.sonar.server.es.IndexDefinitions;
+import org.sonar.server.es.RecoveryIndexer;
 import org.sonar.server.event.NewAlerts;
 import org.sonar.server.favorite.FavoriteModule;
 import org.sonar.server.issue.AddTagsAction;
@@ -520,7 +521,9 @@ public class PlatformLevel4 extends PlatformLevel {
       WebhooksWsModule.class,
 
       // Http Request ID
-      HttpRequestIdModule.class);
+      HttpRequestIdModule.class,
+
+      RecoveryIndexer.class);
     addAll(level4AddedComponents);
   }
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/property/InternalProperties.java b/server/sonar-server/src/main/java/org/sonar/server/property/InternalProperties.java
index 1d84ad9441a..42925dbb097 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/property/InternalProperties.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/property/InternalProperties.java
@@ -34,6 +34,8 @@ public interface InternalProperties {
 
   String ORGANIZATION_ENABLED = "organization.enabled";
 
+  String ES_INDEX_INITIALIZING_PREFIX = "es.initializing.";
+
   /**
    * Read the value of the specified property.
    *
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexer.java
index 1cd6a8f8c63..f61b6089a68 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexer.java
@@ -58,7 +58,7 @@ public class ActiveRuleIndexer implements StartupIndexer {
   }
 
   @Override
-  public void indexOnStartup(Set<IndexType> emptyIndexTypes) {
+  public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
     try (DbSession dbSession = dbClient.openSession(false)) {
       ActiveRuleIterator dbCursor = activeRuleIteratorFactory.createForAll(dbSession);
       scrollDbAndIndex(dbCursor, Size.LARGE);
diff --git a/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java
index 4e0eccebfcb..30919221b0a 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java
@@ -77,7 +77,7 @@ public class TestIndexer implements ProjectIndexer, StartupIndexer {
   }
 
   @Override
-  public void indexOnStartup(Set<IndexType> emptyIndexTypes) {
+  public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
     doIndex(null, Size.LARGE);
   }
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java
index b92fae91e36..b29394d3845 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java
@@ -22,16 +22,17 @@ package org.sonar.server.user;
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import java.security.SecureRandom;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.Random;
+import java.util.function.Consumer;
 import javax.annotation.Nullable;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.platform.NewUserHandler;
 import org.sonar.api.server.ServerSide;
-import org.sonar.api.utils.System2;
 import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
@@ -73,19 +74,17 @@ public class UserUpdater {
   private final NewUserNotifier newUserNotifier;
   private final DbClient dbClient;
   private final UserIndexer userIndexer;
-  private final System2 system2;
   private final OrganizationFlags organizationFlags;
   private final DefaultOrganizationProvider defaultOrganizationProvider;
   private final OrganizationCreation organizationCreation;
   private final DefaultGroupFinder defaultGroupFinder;
   private final Configuration config;
 
-  public UserUpdater(NewUserNotifier newUserNotifier, DbClient dbClient, UserIndexer userIndexer, System2 system2, OrganizationFlags organizationFlags,
+  public UserUpdater(NewUserNotifier newUserNotifier, DbClient dbClient, UserIndexer userIndexer, OrganizationFlags organizationFlags,
     DefaultOrganizationProvider defaultOrganizationProvider, OrganizationCreation organizationCreation, DefaultGroupFinder defaultGroupFinder, Configuration config) {
     this.newUserNotifier = newUserNotifier;
     this.dbClient = dbClient;
     this.userIndexer = userIndexer;
-    this.system2 = system2;
     this.organizationFlags = organizationFlags;
     this.defaultOrganizationProvider = defaultOrganizationProvider;
     this.organizationCreation = organizationCreation;
@@ -93,14 +92,17 @@ public class UserUpdater {
     this.config = config;
   }
 
-  public UserDto create(DbSession dbSession, NewUser newUser) {
+  public UserDto createAndCommit(DbSession dbSession, NewUser newUser, Consumer<UserDto> beforeCommit) {
     String login = newUser.login();
     UserDto userDto = dbClient.userDao().selectByLogin(dbSession, newUser.login());
     if (userDto == null) {
-      userDto = saveUser(dbSession, createNewUserDto(dbSession, newUser));
+      userDto = saveUser(dbSession, createDto(dbSession, newUser));
     } else {
       reactivateUser(dbSession, userDto, login, newUser);
     }
+    beforeCommit.accept(userDto);
+    userIndexer.commitAndIndex(dbSession, userDto);
+
     notifyNewUser(userDto.getLogin(), userDto.getName(), newUser.email());
     return userDto;
   }
@@ -120,26 +122,31 @@ public class UserUpdater {
     // Hack to allow to change the password of the user
     existingUser.setLocal(true);
     setOnboarded(existingUser);
-    updateUserDto(dbSession, updateUser, existingUser);
+    updateDto(dbSession, updateUser, existingUser);
     updateUser(dbSession, existingUser);
     addUserToDefaultOrganizationAndDefaultGroup(dbSession, existingUser);
-    dbSession.commit();
   }
 
-  public void update(DbSession dbSession, UpdateUser updateUser) {
-    UserDto user = dbClient.userDao().selectByLogin(dbSession, updateUser.login());
-    checkFound(user, "User with login '%s' has not been found", updateUser.login());
-    boolean isUserUpdated = updateUserDto(dbSession, updateUser, user);
-    if (!isUserUpdated) {
-      return;
+  public void updateAndCommit(DbSession dbSession, UpdateUser updateUser, Consumer<UserDto> beforeCommit) {
+    UserDto dto = dbClient.userDao().selectByLogin(dbSession, updateUser.login());
+    checkFound(dto, "User with login '%s' has not been found", updateUser.login());
+    boolean isUserUpdated = updateDto(dbSession, updateUser, dto);
+    if (isUserUpdated) {
+      // at least one change. Database must be updated and Elasticsearch re-indexed
+      updateUser(dbSession, dto);
+      beforeCommit.accept(dto);
+      userIndexer.commitAndIndex(dbSession, dto);
+      notifyNewUser(dto.getLogin(), dto.getName(), dto.getEmail());
+    } else {
+      // no changes but still execute the consumer
+      beforeCommit.accept(dto);
+      dbSession.commit();
     }
-    updateUser(dbSession, user);
-    notifyNewUser(user.getLogin(), user.getName(), user.getEmail());
   }
 
-  private UserDto createNewUserDto(DbSession dbSession, NewUser newUser) {
+  private UserDto createDto(DbSession dbSession, NewUser newUser) {
     UserDto userDto = new UserDto();
-    List<String> messages = newArrayList();
+    List<String> messages = new ArrayList<>();
 
     String login = newUser.login();
     if (validateLoginFormat(login, messages)) {
@@ -158,7 +165,7 @@ public class UserUpdater {
 
     String password = newUser.password();
     if (password != null && validatePasswords(password, messages)) {
-      setEncryptedPassWord(password, userDto);
+      setEncryptedPassword(password, userDto);
     }
 
     List<String> scmAccounts = sanitizeScmAccounts(newUser.scmAccounts());
@@ -173,13 +180,13 @@ public class UserUpdater {
     return userDto;
   }
 
-  private boolean updateUserDto(DbSession dbSession, UpdateUser updateUser, UserDto userDto) {
+  private boolean updateDto(DbSession dbSession, UpdateUser update, UserDto dto) {
     List<String> messages = newArrayList();
-    boolean changed = updateName(updateUser, userDto, messages);
-    changed |= updateEmail(updateUser, userDto, messages);
-    changed |= updateExternalIdentity(updateUser, userDto);
-    changed |= updatePassword(updateUser, userDto, messages);
-    changed |= updateScmAccounts(dbSession, updateUser, userDto, messages);
+    boolean changed = updateName(update, dto, messages);
+    changed |= updateEmail(update, dto, messages);
+    changed |= updateExternalIdentity(update, dto);
+    changed |= updatePassword(update, dto, messages);
+    changed |= updateScmAccounts(dbSession, update, dto, messages);
     checkRequest(messages.isEmpty(), messages);
     return changed;
   }
@@ -216,7 +223,7 @@ public class UserUpdater {
   private static boolean updatePassword(UpdateUser updateUser, UserDto userDto, List<String> messages) {
     String password = updateUser.password();
     if (!updateUser.isExternalIdentityChanged() && updateUser.isPasswordChanged() && validatePasswords(password, messages) && checkPasswordChangeAllowed(userDto, messages)) {
-      setEncryptedPassWord(password, userDto);
+      setEncryptedPassword(password, userDto);
       return true;
     }
     return false;
@@ -355,25 +362,19 @@ public class UserUpdater {
   }
 
   private UserDto saveUser(DbSession dbSession, UserDto userDto) {
-    long now = system2.now();
-    userDto.setActive(true).setCreatedAt(now).setUpdatedAt(now);
+    userDto.setActive(true);
     UserDto res = dbClient.userDao().insert(dbSession, userDto);
     addUserToDefaultOrganizationAndDefaultGroup(dbSession, userDto);
     organizationCreation.createForUser(dbSession, userDto);
-    dbSession.commit();
-    userIndexer.index(userDto.getLogin());
     return res;
   }
 
-  private void updateUser(DbSession dbSession, UserDto userDto) {
-    long now = system2.now();
-    userDto.setActive(true).setUpdatedAt(now);
-    dbClient.userDao().update(dbSession, userDto);
-    dbSession.commit();
-    userIndexer.index(userDto.getLogin());
+  private void updateUser(DbSession dbSession, UserDto dto) {
+    dto.setActive(true);
+    dbClient.userDao().update(dbSession, dto);
   }
 
-  private static void setEncryptedPassWord(String password, UserDto userDto) {
+  private static void setEncryptedPassword(String password, UserDto userDto) {
     Random random = new SecureRandom();
     byte[] salt = new byte[32];
     random.nextBytes(salt);
@@ -382,7 +383,7 @@ public class UserUpdater {
     userDto.setCryptedPassword(encryptPassword(password, saltHex));
   }
 
-  private void notifyNewUser(String login, String name, String email) {
+  private void notifyNewUser(String login, String name, @Nullable String email) {
     newUserNotifier.onNewUser(NewUserHandler.Context.builder()
       .setLogin(login)
       .setName(name)
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserDoc.java b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserDoc.java
index 1b7591d0f55..153619e2d3f 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserDoc.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserDoc.java
@@ -20,7 +20,6 @@
 package org.sonar.server.user.index;
 
 import com.google.common.collect.Maps;
-import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
@@ -83,14 +82,6 @@ public class UserDoc extends BaseDoc implements User {
     return getField(FIELD_ORGANIZATION_UUIDS);
   }
 
-  public long createdAt() {
-    return getFieldAsDate(UserIndexDefinition.FIELD_CREATED_AT).getTime();
-  }
-
-  public long updatedAt() {
-    return getFieldAsDate(UserIndexDefinition.FIELD_UPDATED_AT).getTime();
-  }
-
   public UserDoc setLogin(@Nullable String s) {
     setField(UserIndexDefinition.FIELD_LOGIN, s);
     return this;
@@ -120,14 +111,4 @@ public class UserDoc extends BaseDoc implements User {
     setField(FIELD_ORGANIZATION_UUIDS, organizationUuids);
     return this;
   }
-
-  public UserDoc setCreatedAt(long l) {
-    setField(UserIndexDefinition.FIELD_CREATED_AT, new Date(l));
-    return this;
-  }
-
-  public UserDoc setUpdatedAt(long l) {
-    setField(UserIndexDefinition.FIELD_UPDATED_AT, new Date(l));
-    return this;
-  }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexDefinition.java b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexDefinition.java
index d64f2fd8145..4178d607ead 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexDefinition.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexDefinition.java
@@ -36,8 +36,6 @@ public class UserIndexDefinition implements IndexDefinition {
   public static final String FIELD_LOGIN = "login";
   public static final String FIELD_NAME = "name";
   public static final String FIELD_EMAIL = "email";
-  public static final String FIELD_CREATED_AT = "createdAt";
-  public static final String FIELD_UPDATED_AT = "updatedAt";
   public static final String FIELD_ACTIVE = "active";
   public static final String FIELD_SCM_ACCOUNTS = "scmAccounts";
   public static final String FIELD_ORGANIZATION_UUIDS = "organizationUuids";
@@ -59,8 +57,6 @@ public class UserIndexDefinition implements IndexDefinition {
     mapping.stringFieldBuilder(FIELD_LOGIN).addSubFields(USER_SEARCH_GRAMS_ANALYZER).build();
     mapping.stringFieldBuilder(FIELD_NAME).addSubFields(USER_SEARCH_GRAMS_ANALYZER).build();
     mapping.stringFieldBuilder(FIELD_EMAIL).addSubFields(USER_SEARCH_GRAMS_ANALYZER, SORTABLE_ANALYZER).build();
-    mapping.createDateTimeField(FIELD_CREATED_AT);
-    mapping.createDateTimeField(FIELD_UPDATED_AT);
     mapping.createBooleanField(FIELD_ACTIVE);
     mapping.stringFieldBuilder(FIELD_SCM_ACCOUNTS).disableNorms().addSubFields(SORTABLE_ANALYZER).build();
     mapping.stringFieldBuilder(FIELD_ORGANIZATION_UUIDS).disableNorms().build();
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java
index 65241119240..6f99292e7cb 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java
@@ -19,14 +19,20 @@
  */
 package org.sonar.server.user.index;
 
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableSet;
-import java.util.Iterator;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
-import javax.annotation.Nullable;
 import org.elasticsearch.action.index.IndexRequest;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
+import org.sonar.db.user.UserDto;
 import org.sonar.server.es.BulkIndexer;
 import org.sonar.server.es.BulkIndexer.Size;
 import org.sonar.server.es.EsClient;
@@ -35,7 +41,7 @@ import org.sonar.server.es.StartupIndexer;
 
 import static java.util.Collections.singletonList;
 import static java.util.Objects.requireNonNull;
-import static org.sonar.db.DatabaseUtils.executeLargeInputsWithoutOutput;
+import static org.sonar.core.util.stream.MoreCollectors.toHashSet;
 import static org.sonar.server.user.index.UserIndexDefinition.INDEX_TYPE_USER;
 
 public class UserIndexer implements StartupIndexer {
@@ -54,56 +60,98 @@ public class UserIndexer implements StartupIndexer {
   }
 
   @Override
-  public void indexOnStartup(Set<IndexType> emptyIndexTypes) {
-    doIndex(newBulkIndexer(Size.LARGE), null);
+  public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      ListMultimap<String, String> organizationUuidsByLogin = ArrayListMultimap.create();
+      dbClient.organizationMemberDao().selectAllForUserIndexing(dbSession, organizationUuidsByLogin::put);
+
+      BulkIndexer bulkIndexer = newBulkIndexer(Size.LARGE);
+      bulkIndexer.start();
+      dbClient.userDao().scrollAll(dbSession,
+        // only index requests, no deletion requests.
+        // Deactivated users are not deleted but updated.
+        u -> bulkIndexer.add(newIndexRequest(u, organizationUuidsByLogin)));
+      bulkIndexer.stop();
+    }
   }
 
-  public void index(String login) {
-    requireNonNull(login);
-    doIndex(newBulkIndexer(Size.REGULAR), singletonList(login));
+  public void commitAndIndex(DbSession dbSession, UserDto user) {
+    commitAndIndexByLogins(dbSession, singletonList(user.getLogin()));
   }
 
-  public void index(List<String> logins) {
-    requireNonNull(logins);
-    if (logins.isEmpty()) {
-      return;
-    }
-
-    doIndex(newBulkIndexer(Size.REGULAR), logins);
+  public void commitAndIndex(DbSession dbSession, Collection<UserDto> users) {
+    commitAndIndexByLogins(dbSession, Collections2.transform(users, UserDto::getLogin));
   }
 
-  private void doIndex(BulkIndexer bulk, @Nullable List<String> logins) {
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      if (logins == null) {
-        processLogins(bulk, dbSession, null);
-      } else {
-        executeLargeInputsWithoutOutput(logins, l -> processLogins(bulk, dbSession, l));
-      }
-    }
+  public void commitAndIndexByLogins(DbSession dbSession, Collection<String> logins) {
+    List<EsQueueDto> items = logins.stream()
+      .map(l -> EsQueueDto.create(EsQueueDto.Type.USER, l))
+      .collect(MoreCollectors.toArrayList());
+
+    dbClient.esQueueDao().insert(dbSession, items);
+    dbSession.commit();
+    postCommit(dbSession, logins, items);
   }
 
-  private void processLogins(BulkIndexer bulk, DbSession dbSession, @Nullable List<String> logins) {
-    try (UserResultSetIterator rowIt = UserResultSetIterator.create(dbClient, dbSession, logins)) {
-      processResultSet(bulk, rowIt);
-    }
+  /**
+   * Entry point for Byteman tests. See directory tests/resilience.
+   * The parameter "logins" is used only by the Byteman script.
+   */
+  private void postCommit(DbSession dbSession, Collection<String> logins, Collection<EsQueueDto> items) {
+    index(dbSession, items);
   }
 
-  private static void processResultSet(BulkIndexer bulk, Iterator<UserDoc> users) {
-    bulk.start();
-    while (users.hasNext()) {
-      UserDoc user = users.next();
-      bulk.add(newIndexRequest(user));
+  /**
+   * @return the number of items that have been successfully indexed
+   */
+  public long index(DbSession dbSession, Collection<EsQueueDto> items) {
+    if (items.isEmpty()) {
+      return 0L;
     }
-    bulk.stop();
+    Set<String> logins = items
+      .stream()
+      .filter(i -> {
+        requireNonNull(i.getDocUuid(), () -> "BUG - " + i + " has not been persisted before indexing");
+        return true;
+      })
+      .map(EsQueueDto::getDocUuid)
+      .collect(toHashSet(items.size()));
+
+    ListMultimap<String, String> organizationUuidsByLogin = ArrayListMultimap.create();
+    dbClient.organizationMemberDao().selectForUserIndexing(dbSession, logins, organizationUuidsByLogin::put);
+
+    BulkIndexer bulkIndexer = newBulkIndexer(Size.REGULAR);
+    bulkIndexer.start(dbSession, dbClient, items);
+    dbClient.userDao().scrollByLogins(dbSession, logins,
+      // only index requests, no deletion requests.
+      // Deactivated users are not deleted but updated.
+      u -> {
+        logins.remove(u.getLogin());
+        bulkIndexer.add(newIndexRequest(u, organizationUuidsByLogin));
+      });
+
+    // the remaining logins reference rows that don't exist in db. They must
+    // be deleted from index.
+    logins.forEach(l -> bulkIndexer.addDeletion(UserIndexDefinition.INDEX_TYPE_USER, l));
+    return bulkIndexer.stop();
   }
 
   private BulkIndexer newBulkIndexer(Size bulkSize) {
     return new BulkIndexer(esClient, UserIndexDefinition.INDEX_TYPE_USER.getIndex(), bulkSize);
   }
 
-  private static IndexRequest newIndexRequest(UserDoc user) {
-    return new IndexRequest(UserIndexDefinition.INDEX_TYPE_USER.getIndex(), UserIndexDefinition.INDEX_TYPE_USER.getType(), user.login())
-      .source(user.getFields());
+  private static IndexRequest newIndexRequest(UserDto user, ListMultimap<String, String> organizationUuidsByLogins) {
+    UserDoc doc = new UserDoc(Maps.newHashMapWithExpectedSize(8));
+    // all the keys must be present, even if value is null
+    doc.setLogin(user.getLogin());
+    doc.setName(user.getName());
+    doc.setEmail(user.getEmail());
+    doc.setActive(user.isActive());
+    doc.setScmAccounts(UserDto.decodeScmAccounts(user.getScmAccounts()));
+    doc.setOrganizationUuids(organizationUuidsByLogins.get(user.getLogin()));
+
+    return new IndexRequest(UserIndexDefinition.INDEX_TYPE_USER.getIndex(), UserIndexDefinition.INDEX_TYPE_USER.getType())
+      .id(doc.getId())
+      .source(doc.getFields());
   }
-
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserResultSetIterator.java b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserResultSetIterator.java
index dc1a707310d..3bdf905f729 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserResultSetIterator.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserResultSetIterator.java
@@ -19,22 +19,25 @@
  */
 package org.sonar.server.user.index;
 
-import com.google.common.base.Joiner;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Maps;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.Collection;
 import java.util.List;
-import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import org.apache.commons.lang.StringUtils;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.ResultSetIterator;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.db.user.UserDto;
 
+import static org.apache.commons.lang.StringUtils.repeat;
+import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
+
 /**
  * Scrolls over table USERS and reads documents to populate the user index
  */
@@ -52,8 +55,6 @@ class UserResultSetIterator extends ResultSetIterator<UserDoc> {
   };
 
   private static final String SQL_ALL = "select " + StringUtils.join(FIELDS, ",") + " from users u ";
-  private static final String LOGIN_FILTER = "u.login=?";
-  private static final Joiner OR_JOINER = Joiner.on(" or ");
 
   private final ListMultimap<String, String> organizationUuidsByLogins;
 
@@ -62,16 +63,25 @@ class UserResultSetIterator extends ResultSetIterator<UserDoc> {
     this.organizationUuidsByLogins = organizationUuidsByLogins;
   }
 
-  static UserResultSetIterator create(DbClient dbClient, DbSession session, @Nullable List<String> logins) {
+  static UserResultSetIterator create(DbClient dbClient, DbSession session, @Nullable Collection<EsQueueDto> esQueueDtos) {
     try {
-      String sql = createSql(logins);
+      String sql = SQL_ALL;
+      List<String> logins = null;
+      if (esQueueDtos != null) {
+        logins = esQueueDtos.stream()
+          .filter(i -> i.getDocType() == EsQueueDto.Type.USER)
+          .map(EsQueueDto::getDocUuid).collect(toArrayList());
+        sql += "where (" + repeat("u.login=?", " or ", logins.size()) + ")";
+      }
+
       PreparedStatement stmt = dbClient.getMyBatis().newScrollingSelectStatement(session, sql);
       setParameters(stmt, logins);
 
       ListMultimap<String, String> organizationUuidsByLogin = ArrayListMultimap.create();
-      if (logins == null) {
+      if (esQueueDtos == null) {
         dbClient.organizationMemberDao().selectAllForUserIndexing(session, organizationUuidsByLogin::put);
       } else {
+
         dbClient.organizationMemberDao().selectForUserIndexing(session, logins, organizationUuidsByLogin::put);
       }
 
@@ -81,35 +91,21 @@ class UserResultSetIterator extends ResultSetIterator<UserDoc> {
     }
   }
 
-  private static String createSql(@Nullable List<String> logins) {
-    if (logins == null) {
-      return SQL_ALL;
-    }
-
-    List<String> sqlLogins = logins.stream()
-      .map(l -> LOGIN_FILTER)
-      .collect(Collectors.toList());
-
-    String sql = SQL_ALL;
-    sql += " WHERE ";
-    sql += "(" + OR_JOINER.join(sqlLogins) + ")";
-
-    return sql;
-  }
-
-  private static void setParameters(PreparedStatement stmt, @Nullable List<String> logins) throws SQLException {
+  private static void setParameters(PreparedStatement stmt, @Nullable Collection<String> logins) throws SQLException {
     if (logins == null) {
       return;
     }
 
-    for (int i = 0; i < logins.size(); i++) {
-      stmt.setString(i + 1, logins.get(i));
+    int paramIndex = 1;
+    for (String login : logins) {
+      stmt.setString(paramIndex, login);
+      paramIndex++;
     }
   }
 
   @Override
   protected UserDoc read(ResultSet rs) throws SQLException {
-    UserDoc doc = new UserDoc(Maps.newHashMapWithExpectedSize(8));
+    UserDoc doc = new UserDoc(Maps.newHashMapWithExpectedSize(6));
 
     String login = rs.getString(1);
 
@@ -119,10 +115,7 @@ class UserResultSetIterator extends ResultSetIterator<UserDoc> {
     doc.setEmail(rs.getString(3));
     doc.setActive(rs.getBoolean(4));
     doc.setScmAccounts(UserDto.decodeScmAccounts(rs.getString(5)));
-    doc.setCreatedAt(rs.getLong(6));
-    doc.setUpdatedAt(rs.getLong(7));
     doc.setOrganizationUuids(organizationUuidsByLogins.get(login));
     return doc;
   }
-
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/ChangePasswordAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/ChangePasswordAction.java
index bd973387892..5faca4c8879 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/ChangePasswordAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/ChangePasswordAction.java
@@ -90,7 +90,7 @@ public class ChangePasswordAction implements UsersWsAction {
       String password = request.mandatoryParam(PARAM_PASSWORD);
       UpdateUser updateUser = UpdateUser.create(login).setPassword(password);
 
-      userUpdater.update(dbSession, updateUser);
+      userUpdater.updateAndCommit(dbSession, updateUser, u -> {});
     }
     response.noContent();
   }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java
index d0d43c426e7..7feb6dc48d9 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java
@@ -127,9 +127,8 @@ public class CreateAction implements UsersWsAction {
       if (!request.isLocal()) {
         newUser.setExternalIdentity(new ExternalIdentity(SQ_AUTHORITY, request.getLogin()));
       }
-      UserDto userDto = userUpdater.create(dbSession, newUser.build());
-      dbSession.commit();
-      return buildResponse(userDto);
+      UserDto createdUser = userUpdater.createAndCommit(dbSession, newUser.build(), u -> {});
+      return buildResponse(createdUser);
     }
   }
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/DeactivateAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
index 1419e4abebc..94ec4c6926a 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
@@ -101,11 +101,10 @@ public class DeactivateAction implements UsersWsAction {
       dbClient.userPermissionDao().deleteByUserId(dbSession, userId);
       dbClient.permissionTemplateDao().deleteUserPermissionsByUserId(dbSession, userId);
       dbClient.organizationMemberDao().deleteByUserId(dbSession, userId);
-      dbClient.userDao().deactivateUserById(dbSession, userId);
-      dbSession.commit();
+      dbClient.userDao().deactivateUser(dbSession, user);
+      userIndexer.commitAndIndex(dbSession, user);
     }
 
-    userIndexer.index(login);
     writeResponse(response, login);
   }
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/SkipOnboardingTutorialAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/SkipOnboardingTutorialAction.java
index 003208121d2..2caddc3379e 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/SkipOnboardingTutorialAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/SkipOnboardingTutorialAction.java
@@ -35,12 +35,10 @@ public class SkipOnboardingTutorialAction implements UsersWsAction {
 
   private final UserSession userSession;
   private final DbClient dbClient;
-  private final System2 system2;
 
-  public SkipOnboardingTutorialAction(UserSession userSession, DbClient dbClient, System2 system2) {
+  public SkipOnboardingTutorialAction(UserSession userSession, DbClient dbClient) {
     this.userSession = userSession;
     this.dbClient = dbClient;
-    this.system2 = system2;
   }
 
   @Override
@@ -63,7 +61,8 @@ public class SkipOnboardingTutorialAction implements UsersWsAction {
       checkState(userDto != null, "User login '%s' cannot be found", userLogin);
       if (!userDto.isOnboarded()) {
         userDto.setOnboarded(true);
-        userDto.setUpdatedAt(system2.now());
+        // no need to update Elasticsearch, the field onBoarded
+        // is not indexed
         dbClient.userDao().update(dbSession, userDto);
         dbSession.commit();
       }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/UpdateAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/UpdateAction.java
index b9292bd4f73..dc50009b16a 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/UpdateAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/UpdateAction.java
@@ -118,7 +118,7 @@ public class UpdateAction implements UsersWsAction {
     if (!request.getScmAccounts().isEmpty()) {
       updateUser.setScmAccounts(request.getScmAccounts());
     }
-    userUpdater.update(dbSession, updateUser);
+    userUpdater.updateAndCommit(dbSession, updateUser, u -> {});
   }
 
   private void writeUser(DbSession dbSession, Response response, String login) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndexer.java
index 14b5ee7d892..2c92637976c 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndexer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndexer.java
@@ -53,7 +53,7 @@ public class ViewIndexer implements StartupIndexer {
   }
 
   @Override
-  public void indexOnStartup(Set<IndexType> emptyIndexTypes) {
+  public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
     try (DbSession dbSession = dbClient.openSession(false)) {
       Map<String, String> viewAndProjectViewUuidMap = newHashMap();
       for (UuidWithProjectUuidDto uuidWithProjectUuidDto : dbClient.componentDao().selectAllViewsAndSubViews(dbSession)) {
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/SsoAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/SsoAuthenticatorTest.java
index 5875442d41d..6094bac37d4 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/authentication/SsoAuthenticatorTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/SsoAuthenticatorTest.java
@@ -40,12 +40,14 @@ import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
+import org.sonar.server.es.EsTester;
 import org.sonar.server.organization.DefaultOrganizationProvider;
 import org.sonar.server.organization.OrganizationCreation;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.organization.TestOrganizationFlags;
 import org.sonar.server.user.NewUserNotifier;
 import org.sonar.server.user.UserUpdater;
+import org.sonar.server.user.index.UserIndexDefinition;
 import org.sonar.server.user.index.UserIndexer;
 import org.sonar.server.usergroups.DefaultGroupFinder;
 
@@ -65,11 +67,14 @@ import static org.sonar.server.authentication.event.AuthenticationExceptionMatch
 
 public class SsoAuthenticatorTest {
 
+  private MapSettings settings = new MapSettings();
+
   @Rule
   public ExpectedException expectedException = none();
-
   @Rule
   public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
+  @Rule
+  public EsTester es = new EsTester(new UserIndexDefinition(settings.asConfig()));
 
   private static final String DEFAULT_LOGIN = "john";
   private static final String DEFAULT_NAME = "John";
@@ -93,14 +98,14 @@ public class SsoAuthenticatorTest {
   private GroupDto sonarUsers;
 
   private System2 system2 = mock(System2.class);
-  private MapSettings settings = new MapSettings();
   private OrganizationCreation organizationCreation = mock(OrganizationCreation.class);
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
 
+  private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
   private UserIdentityAuthenticator userIdentityAuthenticator = new UserIdentityAuthenticator(
     db.getDbClient(),
-    new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), mock(UserIndexer.class), System2.INSTANCE, organizationFlags, defaultOrganizationProvider, organizationCreation,
+    new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), userIndexer, organizationFlags, defaultOrganizationProvider, organizationCreation,
       new DefaultGroupFinder(db.getDbClient()), settings.asConfig()),
     defaultOrganizationProvider, organizationFlags, new DefaultGroupFinder(db.getDbClient()));
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java
index 40638401375..1371534a98f 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java
@@ -35,12 +35,14 @@ import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.authentication.event.AuthenticationEvent.Method;
 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
+import org.sonar.server.es.EsTester;
 import org.sonar.server.organization.DefaultOrganizationProvider;
 import org.sonar.server.organization.OrganizationCreation;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.organization.TestOrganizationFlags;
 import org.sonar.server.user.NewUserNotifier;
 import org.sonar.server.user.UserUpdater;
+import org.sonar.server.user.index.UserIndexDefinition;
 import org.sonar.server.user.index.UserIndexer;
 import org.sonar.server.usergroups.DefaultGroupFinder;
 
@@ -69,32 +71,33 @@ public class UserIdentityAuthenticatorTest {
     .setEnabled(true)
     .setAllowsUsersToSignUp(true);
 
+  private MapSettings settings = new MapSettings();
+
   @Rule
   public ExpectedException thrown = ExpectedException.none();
-
   @Rule
   public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
-
+  @Rule
+  public EsTester es = new EsTester(new UserIndexDefinition(settings.asConfig()));
+  private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private OrganizationCreation organizationCreation = mock(OrganizationCreation.class);
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
-  private MapSettings settings = new MapSettings();
-
   private UserUpdater userUpdater = new UserUpdater(
     mock(NewUserNotifier.class),
     db.getDbClient(),
-    mock(UserIndexer.class),
-    System2.INSTANCE,
+    userIndexer,
     organizationFlags,
     defaultOrganizationProvider,
     organizationCreation,
     new DefaultGroupFinder(db.getDbClient()),
     settings.asConfig());
+
   private UserIdentityAuthenticator underTest = new UserIdentityAuthenticator(db.getDbClient(), userUpdater, defaultOrganizationProvider, organizationFlags,
     new DefaultGroupFinder(db.getDbClient()));
 
   @Test
-  public void authenticate_new_user() throws Exception {
+  public void authenticate_new_user() {
     organizationFlags.setEnabled(true);
     underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName()));
 
@@ -111,7 +114,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void authenticate_new_user_with_groups() throws Exception {
+  public void authenticate_new_user_with_groups() {
     organizationFlags.setEnabled(true);
     GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
     GroupDto group2 = db.users().insertGroup(db.getDefaultOrganization(), "group2");
@@ -123,7 +126,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void authenticate_new_user_and_force_default_group_when_organizations_are_disabled() throws Exception {
+  public void authenticate_new_user_and_force_default_group_when_organizations_are_disabled() {
     organizationFlags.setEnabled(false);
     UserDto user = db.users().insertUser();
     GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
@@ -137,7 +140,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void does_not_force_default_group_when_authenticating_new_user_if_organizations_are_enabled() throws Exception {
+  public void does_not_force_default_group_when_authenticating_new_user_if_organizations_are_enabled() {
     organizationFlags.setEnabled(true);
     UserDto user = db.users().insertUser();
     GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
@@ -171,7 +174,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void authenticate_existing_user() throws Exception {
+  public void authenticate_existing_user() {
     db.users().insertUser(newUserDto()
       .setLogin(USER_LOGIN)
       .setActive(true)
@@ -192,7 +195,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void authenticate_existing_disabled_user() throws Exception {
+  public void authenticate_existing_disabled_user() {
     organizationFlags.setEnabled(true);
     db.users().insertUser(newUserDto()
       .setLogin(USER_LOGIN)
@@ -214,7 +217,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void authenticate_existing_user_and_add_new_groups() throws Exception {
+  public void authenticate_existing_user_and_add_new_groups() {
     organizationFlags.setEnabled(true);
     UserDto user = db.users().insertUser(newUserDto()
       .setLogin(USER_LOGIN)
@@ -229,7 +232,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void authenticate_existing_user_and_remove_groups() throws Exception {
+  public void authenticate_existing_user_and_remove_groups() {
     organizationFlags.setEnabled(true);
     UserDto user = db.users().insertUser(newUserDto()
       .setLogin(USER_LOGIN)
@@ -246,7 +249,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void authenticate_existing_user_and_remove_all_groups_expect_default_when_organizations_are_disabled() throws Exception {
+  public void authenticate_existing_user_and_remove_all_groups_expect_default_when_organizations_are_disabled() {
     organizationFlags.setEnabled(false);
     UserDto user = db.users().insertUser();
     GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
@@ -262,7 +265,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void does_not_force_default_group_when_authenticating_existing_user_when_organizations_are_enabled() throws Exception {
+  public void does_not_force_default_group_when_authenticating_existing_user_when_organizations_are_enabled() {
     organizationFlags.setEnabled(true);
     UserDto user = db.users().insertUser();
     GroupDto group1 = db.users().insertGroup(db.getDefaultOrganization(), "group1");
@@ -276,7 +279,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void ignore_groups_on_non_default_organizations() throws Exception {
+  public void ignore_groups_on_non_default_organizations() {
     organizationFlags.setEnabled(true);
     OrganizationDto org = db.organizations().insert();
     UserDto user = db.users().insertUser(newUserDto()
@@ -299,7 +302,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void fail_to_authenticate_new_user_when_allow_users_to_signup_is_false() throws Exception {
+  public void fail_to_authenticate_new_user_when_allow_users_to_signup_is_false() {
     TestIdentityProvider identityProvider = new TestIdentityProvider()
       .setKey("github")
       .setName("Github")
@@ -313,7 +316,7 @@ public class UserIdentityAuthenticatorTest {
   }
 
   @Test
-  public void fail_to_authenticate_new_user_when_email_already_exists() throws Exception {
+  public void fail_to_authenticate_new_user_when_email_already_exists() {
     db.users().insertUser(newUserDto()
       .setLogin("Existing user with same email")
       .setActive(true)
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java
index 00a6028c46b..a1c5519384a 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java
@@ -20,13 +20,21 @@
 package org.sonar.server.es;
 
 import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.stream.IntStream;
+import org.apache.commons.lang.math.RandomUtils;
 import org.elasticsearch.action.admin.indices.settings.get.GetSettingsResponse;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.elasticsearch.cluster.metadata.IndexMetaData;
 import org.elasticsearch.index.query.QueryBuilders;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
+import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.db.DbTester;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.server.es.BulkIndexer.Size;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -35,8 +43,12 @@ import static org.sonar.server.es.FakeIndexDefinition.INDEX_TYPE_FAKE;
 
 public class BulkIndexerTest {
 
+  private TestSystem2 testSystem2 = new TestSystem2().setNow(1_000L);
+
   @Rule
   public EsTester esTester = new EsTester(new FakeIndexDefinition().setReplicas(1));
+  @Rule
+  public DbTester dbTester = DbTester.create(testSystem2);
 
   @Test
   public void index_nothing() {
@@ -102,6 +114,42 @@ public class BulkIndexerTest {
     assertThat(count()).isEqualTo(removeFrom);
   }
 
+  @Test
+  @Ignore
+  public void when_index_is_done_EsQueues_must_be_deleted() {
+    BulkIndexer indexer = new BulkIndexer(esTester.client(), INDEX, Size.REGULAR);
+    int nbOfDelete = 10 + RandomUtils.nextInt(10);
+    int nbOfInsert = 10 + RandomUtils.nextInt(10);
+    int nbOfDocumentNotToBeDeleted = 10 + RandomUtils.nextInt(10);
+    Collection<EsQueueDto> esQueueDtos = new ArrayList<>();
+
+    // Those documents must be kept
+    FakeDoc[] docs = new FakeDoc[nbOfDocumentNotToBeDeleted];
+    for (int i = 1; i <= nbOfDocumentNotToBeDeleted; i++) {
+      docs[i] = FakeIndexDefinition.newDoc(-i);
+    }
+    esTester.putDocuments(INDEX_TYPE_FAKE, docs);
+
+    // Create nbOfDelete documents to be deleted
+    docs = new FakeDoc[nbOfDelete];
+    for (int i = 1; i <= nbOfDelete; i++) {
+      docs[i] = FakeIndexDefinition.newDoc(i);
+    }
+    esTester.putDocuments(INDEX_TYPE_FAKE, docs);
+    assertThat(count()).isEqualTo(nbOfDelete + nbOfDocumentNotToBeDeleted);
+
+    indexer.start(dbTester.getSession(), dbTester.getDbClient(), esQueueDtos);
+    // Create nbOfDelete for old Documents
+    IntStream.rangeClosed(1, nbOfDelete).forEach(
+      i -> indexer.addDeletion(INDEX_TYPE_FAKE, "" + i));
+    // Create nbOfInsert for new Documents
+    IntStream.rangeClosed(nbOfDelete + 1, nbOfInsert).forEach(
+      i -> indexer.add(newIndexRequest(i)));
+    indexer.stop();
+
+    assertThat(count()).isEqualTo(nbOfInsert + nbOfDocumentNotToBeDeleted);
+  }
+
   private long count() {
     return esTester.countDocuments("fakes", "fake");
   }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/RecoveryIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/RecoveryIndexerTest.java
new file mode 100644
index 00000000000..bb43319c9e4
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/es/RecoveryIndexerTest.java
@@ -0,0 +1,394 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.es;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.IntStream;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.sonar.api.config.Settings;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.es.EsQueueDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.user.index.UserIndexDefinition;
+import org.sonar.server.user.index.UserIndexer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.sonar.api.utils.log.LoggerLevel.ERROR;
+import static org.sonar.api.utils.log.LoggerLevel.INFO;
+import static org.sonar.api.utils.log.LoggerLevel.TRACE;
+import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
+
+public class RecoveryIndexerTest {
+
+  private static final long PAST = 1_000L;
+  private TestSystem2 system2 = new TestSystem2().setNow(PAST);
+
+  @Rule
+  public final EsTester es = new EsTester(new UserIndexDefinition(new MapSettings().asConfig()));
+  @Rule
+  public final DbTester db = DbTester.create(system2);
+  @Rule
+  public final LogTester logTester = new LogTester().setLevel(TRACE);
+  @Rule
+  public TestRule safeguard = new Timeout(60, TimeUnit.SECONDS);
+
+  private RecoveryIndexer underTest;
+
+  @After
+  public void tearDown() {
+    if (underTest != null) {
+      underTest.stop();
+    }
+  }
+
+  @Test
+  public void display_default_configuration_at_startup() {
+    UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
+    underTest = newRecoveryIndexer(userIndexer, new MapSettings());
+
+    underTest.start();
+
+    assertThat(logTester.logs(LoggerLevel.DEBUG)).contains(
+      "Elasticsearch recovery - sonar.search.recovery.delayInMs=300000",
+      "Elasticsearch recovery - sonar.search.recovery.minAgeInMs=300000");
+  }
+
+  @Test
+  public void start_triggers_recovery_run_at_fixed_rate() throws Exception {
+    Settings settings = new MapSettings()
+      .setProperty("sonar.search.recovery.initialDelayInMs", "0")
+      .setProperty("sonar.search.recovery.delayInMs", "1");
+    underTest = spy(new RecoveryIndexer(system2, settings, db.getDbClient(), mock(UserIndexer.class)));
+    AtomicInteger calls = new AtomicInteger(0);
+    doAnswer(invocation -> {
+      calls.incrementAndGet();
+      return null;
+    }).when(underTest).recover();
+
+    underTest.start();
+
+    // wait for 2 runs
+    while (calls.get() < 2) {
+      Thread.sleep(1L);
+    }
+  }
+
+  @Test
+  public void successfully_index_old_records() {
+    EsQueueDto item1 = createUnindexedUser();
+    EsQueueDto item2 = createUnindexedUser();
+
+    ProxyUserIndexer userIndexer = new ProxyUserIndexer();
+    advanceInTime();
+    underTest = newRecoveryIndexer(userIndexer);
+    underTest.recover();
+
+    assertThatQueueHasSize(0);
+    assertThat(userIndexer.called)
+      .extracting(EsQueueDto::getUuid)
+      .containsExactlyInAnyOrder(item1.getUuid(), item2.getUuid());
+
+    assertThatLogsContain(TRACE, "Elasticsearch recovery - processing 2 USER");
+    assertThatLogsContain(INFO, "Elasticsearch recovery - 2 documents processed [0 failures]");
+  }
+
+  @Test
+  public void recent_records_are_not_recovered() {
+    createUnindexedUser();
+    createUnindexedUser();
+
+    ProxyUserIndexer userIndexer = new ProxyUserIndexer();
+    // do not advance in time
+    underTest = newRecoveryIndexer(userIndexer);
+    underTest.recover();
+
+    assertThatQueueHasSize(2);
+    assertThat(userIndexer.called).isEmpty();
+
+    assertThatLogsDoNotContain(TRACE, "Elasticsearch recovery - processing 2 USER");
+    assertThatLogsDoNotContain(INFO, "documents processed");
+  }
+
+  @Test
+  public void do_nothing_if_queue_is_empty() {
+    underTest = newRecoveryIndexer();
+
+    underTest.recover();
+
+    assertThatNoLogsFromRecovery(INFO);
+    assertThatNoLogsFromRecovery(ERROR);
+    assertThatQueueHasSize(0);
+  }
+
+  @Test
+  public void log_exception_on_recovery_failure() {
+    createUnindexedUser();
+    FailingOnceUserIndexer failingOnceUserIndexer = new FailingOnceUserIndexer();
+    advanceInTime();
+
+    underTest = newRecoveryIndexer(failingOnceUserIndexer);
+    underTest.recover();
+
+    // No rows treated
+    assertThatQueueHasSize(1);
+    assertThatLogsContain(ERROR, "Elasticsearch recovery - fail to recover documents");
+  }
+
+  @Test
+  public void scheduler_is_not_stopped_on_failures() throws Exception {
+    createUnindexedUser();
+    advanceInTime();
+    FailingUserIndexer userIndexer = new FailingUserIndexer();
+
+    underTest = newRecoveryIndexer(userIndexer);
+    underTest.start();
+
+    // all runs fail, but they are still scheduled
+    // -> waiting for 2 runs
+    while (userIndexer.called.size() < 2) {
+      Thread.sleep(1L);
+    }
+  }
+
+  @Test
+  public void recovery_retries_on_next_run_if_failure() throws Exception {
+    createUnindexedUser();
+    advanceInTime();
+    FailingOnceUserIndexer userIndexer = new FailingOnceUserIndexer();
+
+    underTest = newRecoveryIndexer(userIndexer);
+    underTest.start();
+
+    // first run fails, second run succeeds
+    userIndexer.counter.await(30, TimeUnit.SECONDS);
+
+    // First we expecting an exception at first run
+    // Then the second run must have treated all records
+    assertThatLogsContain(ERROR, "Elasticsearch recovery - fail to recover documents");
+    assertThatQueueHasSize(0);
+  }
+
+  @Test
+  public void stop_run_if_too_many_failures() throws Exception {
+    IntStream.range(0, 10).forEach(i -> createUnindexedUser());
+    advanceInTime();
+
+    // 10 docs to process, by groups of 3.
+    // The first group successfully recovers only 1 docs --> above 30% of failures --> stop run
+    PartiallyFailingUserIndexer failingAboveRatioUserIndexer = new PartiallyFailingUserIndexer(1);
+    Settings settings = new MapSettings()
+      .setProperty("sonar.search.recovery.loopLimit", "3");
+    underTest = newRecoveryIndexer(failingAboveRatioUserIndexer, settings);
+    underTest.recover();
+
+    assertThatLogsContain(ERROR, "Elasticsearch recovery - too many failures [2/3 documents], waiting for next run");
+    assertThatQueueHasSize(9);
+
+    // The indexer must have been called once and only once.
+    assertThat(failingAboveRatioUserIndexer.called).hasSize(3);
+  }
+
+  @Test
+  public void do_not_stop_run_if_success_rate_is_greater_than_ratio() throws Exception {
+    IntStream.range(0, 10).forEach(i -> createUnindexedUser());
+    advanceInTime();
+
+    // 10 docs to process, by groups of 5.
+    // Each group successfully recovers 4 docs --> below 30% of failures --> continue run
+    PartiallyFailingUserIndexer failingAboveRatioUserIndexer = new PartiallyFailingUserIndexer(4, 4, 2);
+    Settings settings = new MapSettings()
+      .setProperty("sonar.search.recovery.loopLimit", "5");
+    underTest = newRecoveryIndexer(failingAboveRatioUserIndexer, settings);
+    underTest.recover();
+
+    assertThatLogsDoNotContain(ERROR, "too many failures");
+    assertThatQueueHasSize(0);
+    assertThat(failingAboveRatioUserIndexer.indexed).hasSize(10);
+    assertThat(failingAboveRatioUserIndexer.called).hasSize(10 + 2 /* retries */);
+  }
+
+  @Test
+  public void failing_always_on_same_document_does_not_generate_infinite_loop() {
+    EsQueueDto buggy = createUnindexedUser();
+    IntStream.range(0, 10).forEach(i -> createUnindexedUser());
+    advanceInTime();
+
+    FailingAlwaysOnSameElementIndexer indexer = new FailingAlwaysOnSameElementIndexer(buggy);
+    underTest = newRecoveryIndexer(indexer);
+    underTest.recover();
+
+    assertThatLogsContain(ERROR, "Elasticsearch recovery - too many failures [1/1 documents], waiting for next run");
+    assertThatQueueHasSize(1);
+  }
+
+  private class ProxyUserIndexer extends UserIndexer {
+    private final List<EsQueueDto> called = new ArrayList<>();
+
+    ProxyUserIndexer() {
+      super(db.getDbClient(), es.client());
+    }
+
+    @Override
+    public long index(DbSession dbSession, Collection<EsQueueDto> items) {
+      called.addAll(items);
+      return super.index(dbSession, items);
+    }
+  }
+
+  private class FailingUserIndexer extends UserIndexer {
+    private final List<EsQueueDto> called = new ArrayList<>();
+
+    FailingUserIndexer() {
+      super(db.getDbClient(), es.client());
+    }
+
+    @Override
+    public long index(DbSession dbSession, Collection<EsQueueDto> items) {
+      called.addAll(items);
+      throw new RuntimeException("boom");
+    }
+
+  }
+
+  private class FailingOnceUserIndexer extends UserIndexer {
+    private final CountDownLatch counter = new CountDownLatch(2);
+
+    FailingOnceUserIndexer() {
+      super(db.getDbClient(), es.client());
+    }
+
+    @Override
+    public long index(DbSession dbSession, Collection<EsQueueDto> items) {
+      try {
+        if (counter.getCount() == 2) {
+          throw new RuntimeException("boom");
+        }
+        return super.index(dbSession, items);
+      } finally {
+        counter.countDown();
+      }
+    }
+  }
+
+  private class FailingAlwaysOnSameElementIndexer extends UserIndexer {
+    private final EsQueueDto failing;
+
+    FailingAlwaysOnSameElementIndexer(EsQueueDto failing) {
+      super(db.getDbClient(), es.client());
+      this.failing = failing;
+    }
+
+    @Override
+    public long index(DbSession dbSession, Collection<EsQueueDto> items) {
+      List<EsQueueDto> filteredItems = items.stream().filter(
+        i -> !i.getUuid().equals(failing.getUuid())).collect(toArrayList());
+      return super.index(dbSession, filteredItems);
+    }
+  }
+
+  private class PartiallyFailingUserIndexer extends UserIndexer {
+    private final List<EsQueueDto> called = new ArrayList<>();
+    private final List<EsQueueDto> indexed = new ArrayList<>();
+    private final Iterator<Integer> successfulReturns;
+
+    PartiallyFailingUserIndexer(int... successfulReturns) {
+      super(db.getDbClient(), es.client());
+      this.successfulReturns = IntStream.of(successfulReturns).iterator();
+    }
+
+    @Override
+    public long index(DbSession dbSession, Collection<EsQueueDto> items) {
+      System.out.println("called with " + items.size());
+      called.addAll(items);
+      int success = successfulReturns.next();
+      items.stream().limit(success).forEach(i -> {
+        System.out.println(" + success");
+        db.getDbClient().esQueueDao().delete(dbSession, i);
+        indexed.add(i);
+      });
+      dbSession.commit();
+      return success;
+    }
+  }
+
+  private void advanceInTime() {
+    system2.setNow(system2.now() + 100_000_000L);
+  }
+
+  private void assertThatLogsContain(LoggerLevel loggerLevel, String message) {
+    assertThat(logTester.logs(loggerLevel)).filteredOn(m -> m.contains(message)).isNotEmpty();
+  }
+
+  private void assertThatLogsDoNotContain(LoggerLevel loggerLevel, String message) {
+    assertThat(logTester.logs(loggerLevel)).filteredOn(m -> m.contains(message)).isEmpty();
+  }
+
+  private void assertThatNoLogsFromRecovery(LoggerLevel loggerLevel) {
+    assertThat(logTester.logs(loggerLevel)).filteredOn(m -> m.contains("Elasticsearch recovery - ")).isEmpty();
+  }
+
+  private void assertThatQueueHasSize(int number) {
+    assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isEqualTo(number);
+  }
+
+  private RecoveryIndexer newRecoveryIndexer() {
+    UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
+    return newRecoveryIndexer(userIndexer);
+  }
+
+  private RecoveryIndexer newRecoveryIndexer(UserIndexer userIndexer) {
+    Settings settings = new MapSettings()
+      .setProperty("sonar.search.recovery.initialDelayInMs", "0")
+      .setProperty("sonar.search.recovery.delayInMs", "1")
+      .setProperty("sonar.search.recovery.minAgeInMs", "1");
+    return newRecoveryIndexer(userIndexer, settings);
+  }
+
+  private RecoveryIndexer newRecoveryIndexer(UserIndexer userIndexer, Settings settings) {
+    return new RecoveryIndexer(system2, settings, db.getDbClient(), userIndexer);
+  }
+
+  private EsQueueDto createUnindexedUser() {
+    UserDto user = db.users().insertUser();
+    EsQueueDto item = EsQueueDto.create(EsQueueDto.Type.USER, user.getLogin());
+    db.getDbClient().esQueueDao().insert(db.getSession(), item);
+    db.commit();
+
+    return item;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java
index 44e487ffc9f..6ba745d2b4d 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java
@@ -20,6 +20,7 @@
 package org.sonar.server.organization;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
 import org.apache.commons.lang.RandomStringUtils;
@@ -113,7 +114,7 @@ public class OrganizationCreationImplTest {
   @Before
   public void setUp() {
     someUser = db.users().insertUser();
-    userIndexer.index(someUser.getLogin());
+    userIndexer.indexOnStartup(new HashSet<>());
   }
 
   @Test
@@ -263,10 +264,8 @@ public class OrganizationCreationImplTest {
   @Test
   public void create_add_current_user_as_member_of_organization() throws OrganizationCreation.KeyConflictException {
     UserDto user = db.users().insertUser();
-    userIndexer.index(user.getLogin());
-
     builtInQProfileRepositoryRule.initialize();
-    userIndexer.index(someUser.getLogin());
+    userIndexer.commitAndIndex(db.getSession(), someUser);
 
     OrganizationDto result = underTest.create(dbSession, someUser, FULL_POPULATED_NEW_ORGANIZATION);
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/CreateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/CreateActionTest.java
index 7d7d33f10d9..e387274390f 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/CreateActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/CreateActionTest.java
@@ -21,6 +21,7 @@ package org.sonar.server.organization.ws;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
 import javax.annotation.Nullable;
@@ -112,7 +113,7 @@ public class CreateActionTest {
   @Before
   public void setUp() {
     user = dbTester.users().insertUser();
-    userIndexer.index(user.getLogin());
+    userIndexer.indexOnStartup(new HashSet<>());
     userSession.logIn(user);
   }
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java
index 903c7fcd16a..117e38fe396 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java
@@ -20,7 +20,6 @@
 
 package org.sonar.server.organization.ws;
 
-import java.util.Arrays;
 import java.util.List;
 import org.junit.Rule;
 import org.junit.Test;
@@ -59,6 +58,7 @@ import org.sonar.server.user.index.UserQuery;
 import org.sonar.server.ws.WsActionTester;
 
 import static com.google.common.collect.ImmutableList.of;
+import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
@@ -305,7 +305,7 @@ public class DeleteActionTest {
     db.organizations().addMember(org, user1);
     db.organizations().addMember(otherOrg, user1);
     db.organizations().addMember(org, user2);
-    userIndexer.index(Arrays.asList(user1.getLogin(), user2.getLogin()));
+    userIndexer.commitAndIndex(db.getSession(), asList(user1, user2));
     logInAsAdministrator(org);
 
     sendRequest(org);
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/RemoveMemberActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/RemoveMemberActionTest.java
index c925621cab5..fe6a84f5132 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/RemoveMemberActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/RemoveMemberActionTest.java
@@ -20,6 +20,7 @@
 
 package org.sonar.server.organization.ws;
 
+import java.util.HashSet;
 import javax.annotation.Nullable;
 import org.junit.Before;
 import org.junit.Rule;
@@ -54,6 +55,7 @@ import org.sonar.server.ws.TestResponse;
 import org.sonar.server.ws.WsActionTester;
 
 import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.groups.Tuple.tuple;
 import static org.sonar.api.CoreProperties.DEFAULT_ISSUE_ASSIGNEE;
@@ -93,11 +95,11 @@ public class RemoveMemberActionTest {
 
     user = db.users().insertUser();
     db.organizations().addMember(organization, user);
-    userIndexer.index(user.getLogin());
 
     UserDto adminUser = db.users().insertAdminByUserPermission(organization);
     db.organizations().addMember(organization, adminUser);
-    userIndexer.index(adminUser.getLogin());
+
+    userIndexer.indexOnStartup(new HashSet<>());
   }
 
   @Test
@@ -317,10 +319,9 @@ public class RemoveMemberActionTest {
     OrganizationDto anotherOrganization = db.organizations().insert();
     UserDto admin1 = db.users().insertAdminByUserPermission(anotherOrganization);
     db.organizations().addMember(anotherOrganization, admin1);
-    userIndexer.index(admin1.getLogin());
     UserDto admin2 = db.users().insertAdminByUserPermission(anotherOrganization);
     db.organizations().addMember(anotherOrganization, admin2);
-    userIndexer.index(admin2.getLogin());
+    userIndexer.commitAndIndex(db.getSession(), asList(admin1, admin2));
 
     call(anotherOrganization.getKey(), admin1.getLogin());
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/SearchMembersActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/SearchMembersActionTest.java
index db4e01c6fbb..ecf87cde61f 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/SearchMembersActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/SearchMembersActionTest.java
@@ -127,7 +127,7 @@ public class SearchMembersActionTest {
   public void return_avatar() {
     UserDto user = db.users().insertUser(u -> u.setEmail("email@domain.com"));
     db.organizations().addMember(db.getDefaultOrganization(), user);
-    indexer.index(user.getLogin());
+    indexer.commitAndIndex(db.getSession(), user);
 
     SearchMembersWsResponse result = call();
 
@@ -195,7 +195,6 @@ public class SearchMembersActionTest {
     IntStream.range(0, 10).forEach(i -> {
       UserDto userDto = db.users().insertUser(user -> user.setName("USER_" + i));
       db.organizations().addMember(db.getDefaultOrganization(), userDto);
-      indexer.index(userDto.getLogin());
     });
     indexAllUsers();
     request.setPage(2).setPageSize(3);
@@ -214,7 +213,6 @@ public class SearchMembersActionTest {
     IntStream.range(0, 10).forEach(i -> {
       UserDto userDto = db.users().insertUser(user -> user.setName("USER_" + i));
       db.organizations().addMember(db.getDefaultOrganization(), userDto);
-      indexer.index(userDto.getLogin());
     });
     indexAllUsers();
     request.setQuery("_9");
@@ -229,7 +227,6 @@ public class SearchMembersActionTest {
     IntStream.range(0, 10).forEach(i -> {
       UserDto userDto = db.users().insertUser(user -> user.setLogin("USER_" + i));
       db.organizations().addMember(db.getDefaultOrganization(), userDto);
-      indexer.index(userDto.getLogin());
     });
     indexAllUsers();
     request.setQuery("_9");
@@ -246,7 +243,6 @@ public class SearchMembersActionTest {
         .setLogin("L" + i)
         .setEmail("USER_" + i + "@email.com"));
       db.organizations().addMember(db.getDefaultOrganization(), userDto);
-      indexer.index(userDto.getLogin());
     });
     indexAllUsers();
     request.setQuery("_9");
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java
index f8a2f50108b..ed73fca1371 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java
@@ -484,7 +484,7 @@ public class ServerUserSessionTest {
 
     session.checkIsSystemAdministrator();
 
-    db.getDbClient().userDao().deactivateUserById(db.getSession(), user.getId());
+    db.getDbClient().userDao().deactivateUser(db.getSession(), user);
     db.commit();
 
     // should fail but succeeds because flag is kept in cache
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterTest.java
index 0a3f827f0a9..18db33a72dc 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterTest.java
@@ -31,7 +31,7 @@ import org.mockito.ArgumentCaptor;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.platform.NewUserHandler;
 import org.sonar.api.utils.System2;
-import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.api.utils.internal.AlwaysIncreasingSystem2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
@@ -49,7 +49,6 @@ import org.sonar.server.user.index.UserIndexDefinition;
 import org.sonar.server.user.index.UserIndexer;
 import org.sonar.server.usergroups.DefaultGroupFinder;
 
-import static com.google.common.collect.Lists.newArrayList;
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -67,11 +66,9 @@ import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
 
 public class UserUpdaterTest {
 
-  private static final long NOW = 1418215735482L;
-  private static final long PAST = 1000000000000L;
   private static final String DEFAULT_LOGIN = "marius";
 
-  private System2 system2 = new TestSystem2().setNow(NOW);
+  private System2 system2 = new AlwaysIncreasingSystem2();
 
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
@@ -91,20 +88,21 @@ public class UserUpdaterTest {
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
   private MapSettings settings = new MapSettings();
-  private UserUpdater underTest = new UserUpdater(newUserNotifier, dbClient, userIndexer, system2, organizationFlags, defaultOrganizationProvider, organizationCreation,
+  private UserUpdater underTest = new UserUpdater(newUserNotifier, dbClient, userIndexer, organizationFlags, defaultOrganizationProvider, organizationCreation,
     new DefaultGroupFinder(dbClient), settings.asConfig());
 
   @Test
   public void create_user() {
     createDefaultGroup();
 
-    UserDto dto = underTest.create(db.getSession(), NewUser.builder()
+    UserDto dto = underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setEmail("user@mail.com")
       .setPassword("PASSWORD")
       .setScmAccounts(ImmutableList.of("u1", "u_1", "User 1"))
-      .build());
+      .build(), u -> {
+      });
 
     assertThat(dto.getId()).isNotNull();
     assertThat(dto.getLogin()).isEqualTo("user");
@@ -116,8 +114,9 @@ public class UserUpdaterTest {
 
     assertThat(dto.getSalt()).isNotNull();
     assertThat(dto.getCryptedPassword()).isNotNull();
-    assertThat(dto.getCreatedAt()).isEqualTo(1418215735482L);
-    assertThat(dto.getUpdatedAt()).isEqualTo(1418215735482L);
+    assertThat(dto.getCreatedAt())
+      .isPositive()
+      .isEqualTo(dto.getUpdatedAt());
 
     assertThat(dbClient.userDao().selectByLogin(session, "user").getId()).isEqualTo(dto.getId());
     List<SearchHit> indexUsers = es.getDocuments(UserIndexDefinition.INDEX_TYPE_USER);
@@ -133,10 +132,11 @@ public class UserUpdaterTest {
   public void create_user_with_minimum_fields() {
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("us")
       .setName("User")
-      .build());
+      .build(), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, "us");
     assertThat(dto.getId()).isNotNull();
@@ -151,11 +151,12 @@ public class UserUpdaterTest {
   public void create_user_with_sq_authority_when_no_authority_set() throws Exception {
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, "user");
     assertThat(dto.getExternalIdentity()).isEqualTo("user");
@@ -167,11 +168,12 @@ public class UserUpdaterTest {
   public void create_user_with_identity_provider() throws Exception {
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setExternalIdentity(new ExternalIdentity("github", "github-user"))
-      .build());
+      .build(), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, "user");
     assertThat(dto.isLocal()).isFalse();
@@ -185,11 +187,12 @@ public class UserUpdaterTest {
   public void create_user_with_sonarqube_external_identity() throws Exception {
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setExternalIdentity(new ExternalIdentity(SQ_AUTHORITY, "user"))
-      .build());
+      .build(), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, "user");
     assertThat(dto.isLocal()).isFalse();
@@ -203,12 +206,13 @@ public class UserUpdaterTest {
   public void create_user_with_scm_accounts_containing_blank_or_null_entries() {
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setPassword("password")
-      .setScmAccounts(newArrayList("u1", "", null))
-      .build());
+      .setScmAccounts(asList("u1", "", null))
+      .build(), u -> {
+      });
 
     assertThat(dbClient.userDao().selectByLogin(session, "user").getScmAccountsAsList()).containsOnly("u1");
   }
@@ -217,12 +221,13 @@ public class UserUpdaterTest {
   public void create_user_with_scm_accounts_containing_one_blank_entry() {
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setPassword("password")
-      .setScmAccounts(newArrayList(""))
-      .build());
+      .setScmAccounts(asList(""))
+      .build(), u -> {
+      });
 
     assertThat(dbClient.userDao().selectByLogin(session, "user").getScmAccounts()).isNull();
   }
@@ -231,12 +236,13 @@ public class UserUpdaterTest {
   public void create_user_with_scm_accounts_containing_duplications() {
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setPassword("password")
-      .setScmAccounts(newArrayList("u1", "u1"))
-      .build());
+      .setScmAccounts(asList("u1", "u1"))
+      .build(), u -> {
+      });
 
     assertThat(dbClient.userDao().selectByLogin(session, "user").getScmAccountsAsList()).containsOnly("u1");
   }
@@ -246,10 +252,11 @@ public class UserUpdaterTest {
     createDefaultGroup();
     settings.setProperty(ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS, false);
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
-      .build());
+      .build(), u -> {
+      });
 
     assertThat(dbClient.userDao().selectByLogin(session, "user").isOnboarded()).isTrue();
   }
@@ -259,10 +266,11 @@ public class UserUpdaterTest {
     createDefaultGroup();
     settings.setProperty(ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS, true);
 
-    UserDto user = underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
-      .build());
+      .build(), u -> {
+      });
 
     assertThat(dbClient.userDao().selectByLogin(session, "user").isOnboarded()).isFalse();
   }
@@ -272,12 +280,13 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Login can't be empty");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(null)
       .setName("Marius")
       .setEmail("marius@mail.com")
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
   }
 
   @Test
@@ -285,12 +294,13 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Use only letters, numbers, and .-_@ please.");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("/marius/")
       .setName("Marius")
       .setEmail("marius@mail.com")
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
   }
 
   @Test
@@ -298,12 +308,13 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Use only letters, numbers, and .-_@ please.");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("mari us")
       .setName("Marius")
       .setEmail("marius@mail.com")
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
   }
 
   @Test
@@ -311,12 +322,13 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Login is too short (minimum is 2 characters)");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("m")
       .setName("Marius")
       .setEmail("marius@mail.com")
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
   }
 
   @Test
@@ -324,12 +336,13 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Login is too long (maximum is 255 characters)");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(Strings.repeat("m", 256))
       .setName("Marius")
       .setEmail("marius@mail.com")
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
   }
 
   @Test
@@ -337,12 +350,13 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Name can't be empty");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName(null)
       .setEmail("marius@mail.com")
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
   }
 
   @Test
@@ -350,12 +364,13 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Name is too long (maximum is 200 characters)");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName(Strings.repeat("m", 201))
       .setEmail("marius@mail.com")
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
   }
 
   @Test
@@ -363,23 +378,25 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Email is too long (maximum is 100 characters)");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius")
       .setEmail(Strings.repeat("m", 101))
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
   }
 
   @Test
   public void fail_to_create_user_with_many_errors() {
     try {
-      underTest.create(db.getSession(), NewUser.builder()
+      underTest.createAndCommit(db.getSession(), NewUser.builder()
         .setLogin("")
         .setName("")
         .setEmail("marius@mail.com")
         .setPassword("")
-        .build());
+        .build(), u -> {
+        });
       fail();
     } catch (BadRequestException e) {
       assertThat(e.errors()).hasSize(3);
@@ -393,13 +410,14 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("The scm account 'jo' is already used by user(s) : 'John (john)'");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius")
       .setEmail("marius@mail.com")
       .setPassword("password")
-      .setScmAccounts(newArrayList("jo"))
-      .build());
+      .setScmAccounts(asList("jo"))
+      .build(), u -> {
+      });
   }
 
   @Test
@@ -410,13 +428,14 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("The scm account 'john@email.com' is already used by user(s) : 'John (john), Technical account (technical-account)'");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius")
       .setEmail("marius@mail.com")
       .setPassword("password")
-      .setScmAccounts(newArrayList("john@email.com"))
-      .build());
+      .setScmAccounts(asList("john@email.com"))
+      .build(), u -> {
+      });
   }
 
   @Test
@@ -424,13 +443,14 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Login and email are automatically considered as SCM accounts");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .setScmAccounts(newArrayList(DEFAULT_LOGIN))
-      .build());
+      .setScmAccounts(asList(DEFAULT_LOGIN))
+      .build(), u -> {
+      });
   }
 
   @Test
@@ -438,26 +458,28 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Login and email are automatically considered as SCM accounts");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .setScmAccounts(newArrayList("marius2@mail.com"))
-      .build());
+      .setScmAccounts(asList("marius2@mail.com"))
+      .build(), u -> {
+      });
   }
 
   @Test
   public void notify_new_user() {
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setEmail("user@mail.com")
       .setPassword("password")
-      .setScmAccounts(newArrayList("u1", "u_1"))
-      .build());
+      .setScmAccounts(asList("u1", "u_1"))
+      .build(), u -> {
+      });
 
     verify(newUserNotifier).onNewUser(newUserHandler.capture());
     assertThat(newUserHandler.getValue().getLogin()).isEqualTo("user");
@@ -470,12 +492,13 @@ public class UserUpdaterTest {
     organizationFlags.setEnabled(false);
     GroupDto defaultGroup = createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setEmail("user@mail.com")
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
 
     Multimap<String, String> groups = dbClient.groupMembershipDao().selectGroupsByLogins(session, asList("user"));
     assertThat(groups.get("user")).containsOnly(defaultGroup.getName());
@@ -486,12 +509,13 @@ public class UserUpdaterTest {
     organizationFlags.setEnabled(true);
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setEmail("user@mail.com")
       .setPassword("password")
-      .build());
+      .build(), u -> {
+      });
 
     Multimap<String, String> groups = dbClient.groupMembershipDao().selectGroupsByLogins(session, asList("user"));
     assertThat(groups.get("user")).isEmpty();
@@ -502,25 +526,27 @@ public class UserUpdaterTest {
     expectedException.expect(IllegalStateException.class);
     expectedException.expectMessage("Default group cannot be found");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setEmail("user@mail.com")
       .setPassword("password")
-      .setScmAccounts(newArrayList("u1", "u_1"))
-      .build());
+      .setScmAccounts(asList("u1", "u_1"))
+      .build(), u -> {
+      });
   }
 
   @Test
   public void create_personal_organization_when_creating_user() {
     createDefaultGroup();
 
-    UserDto dto = underTest.create(db.getSession(), NewUser.builder()
+    UserDto dto = underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setEmail("user@mail.com")
       .setPassword("PASSWORD")
-      .build());
+      .build(), u -> {
+      });
 
     verify(organizationCreation).createForUser(any(DbSession.class), eq(dto));
   }
@@ -530,12 +556,13 @@ public class UserUpdaterTest {
     organizationFlags.setEnabled(false);
     createDefaultGroup();
 
-    UserDto dto = underTest.create(db.getSession(), NewUser.builder()
+    UserDto dto = underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setEmail("user@mail.com")
       .setPassword("PASSWORD")
-      .build());
+      .build(), u -> {
+      });
 
     assertThat(dbClient.organizationMemberDao().select(db.getSession(), defaultOrganizationProvider.get().getUuid(), dto.getId())).isPresent();
   }
@@ -545,30 +572,30 @@ public class UserUpdaterTest {
     organizationFlags.setEnabled(true);
     createDefaultGroup();
 
-    UserDto dto = underTest.create(db.getSession(), NewUser.builder()
+    UserDto dto = underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin("user")
       .setName("User")
       .setEmail("user@mail.com")
       .setPassword("PASSWORD")
-      .build());
+      .build(), u -> {
+      });
 
     assertThat(dbClient.organizationMemberDao().select(db.getSession(), defaultOrganizationProvider.get().getUuid(), dto.getId())).isNotPresent();
   }
 
   @Test
   public void reactivate_user_when_creating_user_with_existing_login() {
-    db.users().insertUser(newDisabledUser(DEFAULT_LOGIN)
-      .setLocal(false)
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+    UserDto user = db.users().insertUser(newDisabledUser(DEFAULT_LOGIN)
+      .setLocal(false));
     createDefaultGroup();
 
-    UserDto dto = underTest.create(db.getSession(), NewUser.builder()
+    UserDto dto = underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .build());
+      .build(), u -> {
+      });
     session.commit();
 
     assertThat(dto.isActive()).isTrue();
@@ -579,26 +606,25 @@ public class UserUpdaterTest {
 
     assertThat(dto.getSalt()).isNotNull().isNotEqualTo("79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365");
     assertThat(dto.getCryptedPassword()).isNotNull().isNotEqualTo("650d2261c98361e2f67f90ce5c65a95e7d8ea2fg");
-    assertThat(dto.getCreatedAt()).isEqualTo(PAST);
-    assertThat(dto.getUpdatedAt()).isEqualTo(NOW);
+    assertThat(dto.getCreatedAt()).isEqualTo(user.getCreatedAt());
+    assertThat(dto.getUpdatedAt()).isGreaterThan(user.getCreatedAt());
 
     assertThat(dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN).isActive()).isTrue();
   }
 
   @Test
   public void reactivate_user_not_having_password() {
-    db.users().insertUser(newDisabledUser("marius").setName("Marius").setEmail("marius@lesbronzes.fr")
+    UserDto user = db.users().insertUser(newDisabledUser("marius").setName("Marius").setEmail("marius@lesbronzes.fr")
       .setSalt(null)
-      .setCryptedPassword(null)
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setCryptedPassword(null));
     createDefaultGroup();
 
-    UserDto dto = underTest.create(db.getSession(), NewUser.builder()
+    UserDto dto = underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
-      .build());
+      .build(), u -> {
+      });
     session.commit();
 
     assertThat(dto.isActive()).isTrue();
@@ -608,23 +634,22 @@ public class UserUpdaterTest {
 
     assertThat(dto.getSalt()).isNull();
     assertThat(dto.getCryptedPassword()).isNull();
-    assertThat(dto.getCreatedAt()).isEqualTo(PAST);
-    assertThat(dto.getUpdatedAt()).isEqualTo(NOW);
+    assertThat(dto.getCreatedAt()).isEqualTo(user.getCreatedAt());
+    assertThat(dto.getUpdatedAt()).isGreaterThan(user.getCreatedAt());
   }
 
   @Test
   public void update_external_provider_when_reactivating_user() {
     db.users().insertUser(newDisabledUser(DEFAULT_LOGIN)
-      .setLocal(true)
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setLocal(true));
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius2")
       .setExternalIdentity(new ExternalIdentity("github", "john"))
-      .build());
+      .build(), u -> {
+      });
     session.commit();
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
@@ -635,40 +660,38 @@ public class UserUpdaterTest {
 
   @Test
   public void fail_to_reactivate_user_if_not_disabled() {
-    db.users().insertUser(newLocalUser("marius", "Marius", "marius@lesbronzes.fr")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+    db.users().insertUser(newLocalUser("marius", "Marius", "marius@lesbronzes.fr"));
     createDefaultGroup();
 
     expectedException.expect(IllegalArgumentException.class);
     expectedException.expectMessage("An active user with login 'marius' already exists");
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .build());
+      .build(), u -> {
+      });
   }
 
   @Test
   public void associate_default_groups_when_reactivating_user_and_organizations_are_disabled() {
     organizationFlags.setEnabled(false);
     UserDto userDto = db.users().insertUser(newDisabledUser(DEFAULT_LOGIN)
-      .setLocal(true)
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setLocal(true));
     db.organizations().insertForUuid("org1");
     GroupDto groupDto = db.users().insertGroup(GroupTesting.newGroupDto().setName("sonar-devs").setOrganizationUuid("org1"));
     db.users().insertMember(groupDto, userDto);
     GroupDto defaultGroup = createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .build());
+      .build(), u -> {
+      });
     session.commit();
 
     Multimap<String, String> groups = dbClient.groupMembershipDao().selectGroupsByLogins(session, asList(DEFAULT_LOGIN));
@@ -679,20 +702,19 @@ public class UserUpdaterTest {
   public void does_not_associate_default_groups_when_reactivating_user_and_organizations_are_enabled() {
     organizationFlags.setEnabled(true);
     UserDto userDto = db.users().insertUser(newDisabledUser(DEFAULT_LOGIN)
-      .setLocal(true)
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setLocal(true));
     db.organizations().insertForUuid("org1");
     GroupDto groupDto = db.users().insertGroup(GroupTesting.newGroupDto().setName("sonar-devs").setOrganizationUuid("org1"));
     db.users().insertMember(groupDto, userDto);
     GroupDto defaultGroup = createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .build());
+      .build(), u -> {
+      });
     session.commit();
 
     Multimap<String, String> groups = dbClient.groupMembershipDao().selectGroupsByLogins(session, asList(DEFAULT_LOGIN));
@@ -705,7 +727,8 @@ public class UserUpdaterTest {
     db.users().insertUser(newDisabledUser(DEFAULT_LOGIN));
     createDefaultGroup();
 
-    UserDto dto = underTest.create(db.getSession(), NewUser.builder().setLogin(DEFAULT_LOGIN).setName("Name").build());
+    UserDto dto = underTest.createAndCommit(db.getSession(), NewUser.builder().setLogin(DEFAULT_LOGIN).setName("Name").build(), u -> {
+    });
     session.commit();
 
     assertThat(dbClient.organizationMemberDao().select(db.getSession(), defaultOrganizationProvider.get().getUuid(), dto.getId())).isPresent();
@@ -717,7 +740,8 @@ public class UserUpdaterTest {
     db.users().insertUser(newDisabledUser(DEFAULT_LOGIN));
     createDefaultGroup();
 
-    UserDto dto = underTest.create(db.getSession(), NewUser.builder().setLogin(DEFAULT_LOGIN).setName("Name").build());
+    UserDto dto = underTest.createAndCommit(db.getSession(), NewUser.builder().setLogin(DEFAULT_LOGIN).setName("Name").build(), u -> {
+    });
     session.commit();
 
     assertThat(dbClient.organizationMemberDao().select(db.getSession(), defaultOrganizationProvider.get().getUuid(), dto.getId())).isNotPresent();
@@ -731,10 +755,11 @@ public class UserUpdaterTest {
       .setOnboarded(false));
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(user.getLogin())
       .setName("name")
-      .build());
+      .build(), u -> {
+      });
 
     assertThat(dbClient.userDao().selectByLogin(session, user.getLogin()).isOnboarded()).isTrue();
   }
@@ -747,10 +772,11 @@ public class UserUpdaterTest {
       .setOnboarded(true));
     createDefaultGroup();
 
-    underTest.create(db.getSession(), NewUser.builder()
+    underTest.createAndCommit(db.getSession(), NewUser.builder()
       .setLogin(user.getLogin())
       .setName("name")
-      .build());
+      .build(), u -> {
+      });
 
     assertThat(dbClient.userDao().selectByLogin(session, user.getLogin()).isOnboarded()).isFalse();
   }
@@ -760,17 +786,15 @@ public class UserUpdaterTest {
     UserDto user = db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
       .setScmAccounts(asList("ma", "marius33"))
       .setSalt("79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365")
-      .setCryptedPassword("650d2261c98361e2f67f90ce5c65a95e7d8ea2fg")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setCryptedPassword("650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .setScmAccounts(newArrayList("ma2")));
-    session.commit();
+      .setScmAccounts(asList("ma2")), u -> {
+      });
 
     UserDto updatedUser = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(updatedUser.isActive()).isTrue();
@@ -780,8 +804,8 @@ public class UserUpdaterTest {
 
     assertThat(updatedUser.getSalt()).isNotEqualTo(user.getSalt());
     assertThat(updatedUser.getCryptedPassword()).isNotEqualTo(user.getCryptedPassword());
-    assertThat(updatedUser.getCreatedAt()).isEqualTo(PAST);
-    assertThat(updatedUser.getUpdatedAt()).isEqualTo(NOW);
+    assertThat(updatedUser.getCreatedAt()).isEqualTo(user.getCreatedAt());
+    assertThat(updatedUser.getUpdatedAt()).isGreaterThan(user.getCreatedAt());
 
     List<SearchHit> indexUsers = es.getDocuments(UserIndexDefinition.INDEX_TYPE_USER);
     assertThat(indexUsers).hasSize(1);
@@ -794,37 +818,33 @@ public class UserUpdaterTest {
 
   @Test
   public void update_user_external_identity_when_user_was_not_local() {
-    db.users().insertUser(UserTesting.newExternalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+    UserDto user = db.users().insertUser(UserTesting.newExternalUser(DEFAULT_LOGIN, "Marius", "marius@email.com"));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@email.com")
       .setPassword(null)
-      .setExternalIdentity(new ExternalIdentity("github", "john")));
-    session.commit();
+      .setExternalIdentity(new ExternalIdentity("github", "john")), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(dto.getExternalIdentity()).isEqualTo("john");
     assertThat(dto.getExternalIdentityProvider()).isEqualTo("github");
-    assertThat(dto.getUpdatedAt()).isEqualTo(NOW);
+    assertThat(dto.getUpdatedAt()).isGreaterThan(user.getCreatedAt());
   }
 
   @Test
   public void update_user_external_identity_when_user_was_local() {
-    db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+    UserDto user = db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@email.com"));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@email.com")
       .setPassword(null)
-      .setExternalIdentity(new ExternalIdentity("github", "john")));
-    session.commit();
+      .setExternalIdentity(new ExternalIdentity("github", "john")), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(dto.getExternalIdentity()).isEqualTo("john");
@@ -832,7 +852,7 @@ public class UserUpdaterTest {
     // Password must be removed
     assertThat(dto.getCryptedPassword()).isNull();
     assertThat(dto.getSalt()).isNull();
-    assertThat(dto.getUpdatedAt()).isEqualTo(NOW);
+    assertThat(dto.getUpdatedAt()).isGreaterThan(user.getCreatedAt());
   }
 
   @Test
@@ -840,17 +860,15 @@ public class UserUpdaterTest {
     UserDto user = db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr")
       .setScmAccounts(asList("ma", "marius33"))
       .setSalt("salt")
-      .setCryptedPassword("crypted password")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setCryptedPassword("crypted password"));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .setScmAccounts(newArrayList("ma2")));
-    session.commit();
+      .setScmAccounts(asList("ma2")), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(dto.isActive()).isTrue();
@@ -860,8 +878,8 @@ public class UserUpdaterTest {
 
     assertThat(dto.getSalt()).isNotEqualTo(user.getSalt());
     assertThat(dto.getCryptedPassword()).isNotEqualTo(user.getCryptedPassword());
-    assertThat(dto.getCreatedAt()).isEqualTo(PAST);
-    assertThat(dto.getUpdatedAt()).isEqualTo(NOW);
+    assertThat(dto.getCreatedAt()).isEqualTo(user.getCreatedAt());
+    assertThat(dto.getUpdatedAt()).isGreaterThan(user.getUpdatedAt());
 
     List<SearchHit> indexUsers = es.getDocuments(UserIndexDefinition.INDEX_TYPE_USER);
     assertThat(indexUsers).hasSize(1);
@@ -875,17 +893,15 @@ public class UserUpdaterTest {
   @Test
   public void update_user_with_scm_accounts_containing_blank_entry() {
     db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr")
-      .setScmAccounts(asList("ma", "marius33"))
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setScmAccounts(asList("ma", "marius33")));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .setScmAccounts(newArrayList("ma2", "", null)));
-    session.commit();
+      .setScmAccounts(asList("ma2", "", null)), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(dto.getScmAccountsAsList()).containsOnly("ma2");
@@ -896,14 +912,12 @@ public class UserUpdaterTest {
     db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr")
       .setScmAccounts(asList("ma", "marius33"))
       .setSalt("salt")
-      .setCryptedPassword("crypted password")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setCryptedPassword("crypted password"));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
-      .setName("Marius2"));
-    session.commit();
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
+      .setName("Marius2"), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(dto.getName()).isEqualTo("Marius2");
@@ -920,14 +934,12 @@ public class UserUpdaterTest {
     db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr")
       .setScmAccounts(asList("ma", "marius33"))
       .setSalt("salt")
-      .setCryptedPassword("crypted password")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setCryptedPassword("crypted password"));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
-      .setEmail("marius2@mail.com"));
-    session.commit();
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
+      .setEmail("marius2@mail.com"), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(dto.getEmail()).isEqualTo("marius2@mail.com");
@@ -944,14 +956,12 @@ public class UserUpdaterTest {
     db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr")
       .setScmAccounts(asList("ma", "marius33"))
       .setSalt("salt")
-      .setCryptedPassword("crypted password")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setCryptedPassword("crypted password"));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
-      .setScmAccounts(newArrayList("ma2")));
-    session.commit();
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
+      .setScmAccounts(asList("ma2")), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(dto.getScmAccountsAsList()).containsOnly("ma2");
@@ -966,14 +976,12 @@ public class UserUpdaterTest {
   @Test
   public void update_scm_accounts_with_same_values() {
     db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr")
-      .setScmAccounts(asList("ma", "marius33"))
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setScmAccounts(asList("ma", "marius33")));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
-      .setScmAccounts(newArrayList("ma", "marius33")));
-    session.commit();
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
+      .setScmAccounts(asList("ma", "marius33")), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(dto.getScmAccountsAsList()).containsOnly("ma", "marius33");
@@ -982,14 +990,12 @@ public class UserUpdaterTest {
   @Test
   public void remove_scm_accounts() {
     db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr")
-      .setScmAccounts(asList("ma", "marius33"))
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setScmAccounts(asList("ma", "marius33")));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
-      .setScmAccounts(null));
-    session.commit();
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
+      .setScmAccounts(null), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(dto.getScmAccounts()).isNull();
@@ -1000,14 +1006,12 @@ public class UserUpdaterTest {
     db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr")
       .setScmAccounts(asList("ma", "marius33"))
       .setSalt("salt")
-      .setCryptedPassword("crypted password")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setCryptedPassword("crypted password"));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
-      .setPassword("password2"));
-    session.commit();
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
+      .setPassword("password2"), u -> {
+      });
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
     assertThat(dto.getSalt()).isNotEqualTo("salt");
@@ -1023,34 +1027,30 @@ public class UserUpdaterTest {
   public void update_only_external_identity_id() {
     db.users().insertUser(UserTesting.newExternalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
       .setExternalIdentity("john")
-      .setExternalIdentityProvider("github")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setExternalIdentityProvider("github"));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN).setExternalIdentity(new ExternalIdentity("github", "john.smith")));
-    session.commit();
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN).setExternalIdentity(new ExternalIdentity("github", "john.smith")), u -> {
+    });
 
     assertThat(dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN))
-      .extracting(UserDto::getExternalIdentity, UserDto::getExternalIdentityProvider, UserDto::getUpdatedAt)
-      .containsOnly("john.smith", "github", NOW);
+      .extracting(UserDto::getExternalIdentity, UserDto::getExternalIdentityProvider)
+      .containsOnly("john.smith", "github");
   }
 
   @Test
   public void update_only_external_identity_provider() {
     db.users().insertUser(UserTesting.newExternalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
       .setExternalIdentity("john")
-      .setExternalIdentityProvider("github")
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST));
+      .setExternalIdentityProvider("github"));
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN).setExternalIdentity(new ExternalIdentity("bitbucket", "john")));
-    session.commit();
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN).setExternalIdentity(new ExternalIdentity("bitbucket", "john")), u -> {
+    });
 
     assertThat(dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN))
-      .extracting(UserDto::getExternalIdentity, UserDto::getExternalIdentityProvider, UserDto::getUpdatedAt)
-      .containsOnly("john", "bitbucket", NOW);
+      .extracting(UserDto::getExternalIdentity, UserDto::getExternalIdentityProvider)
+      .containsOnly("john", "bitbucket");
   }
 
   @Test
@@ -1058,20 +1058,18 @@ public class UserUpdaterTest {
     UserDto user = UserTesting.newExternalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
       .setExternalIdentity("john")
       .setExternalIdentityProvider("github")
-      .setScmAccounts(asList("ma1", "ma2"))
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST);
+      .setScmAccounts(asList("ma1", "ma2"));
     db.users().insertUser(user);
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(user.getLogin())
+    underTest.updateAndCommit(session, UpdateUser.create(user.getLogin())
       .setName(user.getName())
       .setEmail(user.getEmail())
       .setScmAccounts(user.getScmAccountsAsList())
-      .setExternalIdentity(new ExternalIdentity(user.getExternalIdentityProvider(), user.getExternalIdentity())));
-    session.commit();
+      .setExternalIdentity(new ExternalIdentity(user.getExternalIdentityProvider(), user.getExternalIdentity())), u -> {
+      });
 
-    assertThat(dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN).getUpdatedAt()).isEqualTo(PAST);
+    assertThat(dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN).getUpdatedAt()).isEqualTo(user.getUpdatedAt());
   }
 
   @Test
@@ -1079,20 +1077,18 @@ public class UserUpdaterTest {
     UserDto user = UserTesting.newExternalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
       .setExternalIdentity("john")
       .setExternalIdentityProvider("github")
-      .setScmAccounts(asList("ma1", "ma2"))
-      .setCreatedAt(PAST)
-      .setUpdatedAt(PAST);
+      .setScmAccounts(asList("ma1", "ma2"));
     db.users().insertUser(user);
     createDefaultGroup();
 
-    underTest.update(session, UpdateUser.create(user.getLogin())
+    underTest.updateAndCommit(session, UpdateUser.create(user.getLogin())
       .setName(user.getName())
       .setEmail(user.getEmail())
       .setScmAccounts(asList("ma2", "ma1"))
-      .setExternalIdentity(new ExternalIdentity(user.getExternalIdentityProvider(), user.getExternalIdentity())));
-    session.commit();
+      .setExternalIdentity(new ExternalIdentity(user.getExternalIdentityProvider(), user.getExternalIdentity())), u -> {
+      });
 
-    assertThat(dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN).getUpdatedAt()).isEqualTo(PAST);
+    assertThat(dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN).getUpdatedAt()).isEqualTo(user.getUpdatedAt());
   }
 
   @Test
@@ -1102,7 +1098,8 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Password can't be empty");
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN).setPassword(null));
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN).setPassword(null), u -> {
+    });
   }
 
   @Test
@@ -1114,7 +1111,8 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Password cannot be changed when external authentication is used");
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN).setPassword("password2"));
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN).setPassword("password2"), u -> {
+    });
   }
 
   @Test
@@ -1123,12 +1121,12 @@ public class UserUpdaterTest {
     GroupDto defaultGroup = createDefaultGroup();
 
     // Existing user, he has no group, and should not be associated to the default one
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .setScmAccounts(newArrayList("ma2")));
-    session.commit();
+      .setScmAccounts(asList("ma2")), u -> {
+      });
 
     Multimap<String, String> groups = dbClient.groupMembershipDao().selectGroupsByLogins(session, asList(DEFAULT_LOGIN));
     assertThat(groups.get(DEFAULT_LOGIN).stream().anyMatch(g -> g.equals(defaultGroup.getName()))).isFalse();
@@ -1144,12 +1142,12 @@ public class UserUpdaterTest {
     Multimap<String, String> groups = dbClient.groupMembershipDao().selectGroupsByLogins(session, asList(DEFAULT_LOGIN));
     assertThat(groups.get(DEFAULT_LOGIN).stream().anyMatch(g -> g.equals(defaultGroup.getName()))).as("Current user groups : %s", groups.get(defaultGroup.getName())).isTrue();
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .setScmAccounts(newArrayList("ma2")));
-    session.commit();
+      .setScmAccounts(asList("ma2")), u -> {
+      });
 
     // Nothing as changed
     groups = dbClient.groupMembershipDao().selectGroupsByLogins(session, asList(DEFAULT_LOGIN));
@@ -1165,11 +1163,12 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("The scm account 'jo' is already used by user(s) : 'John (john)'");
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .setScmAccounts(newArrayList("jo")));
+      .setScmAccounts(asList("jo")), u -> {
+      });
   }
 
   @Test
@@ -1179,7 +1178,8 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Login and email are automatically considered as SCM accounts");
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN).setScmAccounts(newArrayList(DEFAULT_LOGIN)));
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN).setScmAccounts(asList(DEFAULT_LOGIN)), u -> {
+    });
   }
 
   @Test
@@ -1189,7 +1189,8 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Login and email are automatically considered as SCM accounts");
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN).setScmAccounts(newArrayList("marius@lesbronzes.fr")));
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN).setScmAccounts(asList("marius@lesbronzes.fr")), u -> {
+    });
   }
 
   @Test
@@ -1199,9 +1200,10 @@ public class UserUpdaterTest {
     expectedException.expect(BadRequestException.class);
     expectedException.expectMessage("Login and email are automatically considered as SCM accounts");
 
-    underTest.update(session, UpdateUser.create(DEFAULT_LOGIN)
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
       .setEmail("marius@newmail.com")
-      .setScmAccounts(newArrayList("marius@newmail.com")));
+      .setScmAccounts(asList("marius@newmail.com")), u -> {
+      });
   }
 
   private GroupDto createDefaultGroup() {
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java
index 66ba400145a..f7026e2958f 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java
@@ -67,8 +67,6 @@ public class UserIndexTest {
     assertThat(userDoc.email()).isEqualTo(user1.email());
     assertThat(userDoc.active()).isTrue();
     assertThat(userDoc.scmAccounts()).isEqualTo(user1.scmAccounts());
-    assertThat(userDoc.createdAt()).isEqualTo(user1.createdAt());
-    assertThat(userDoc.updatedAt()).isEqualTo(user1.updatedAt());
 
     assertThat(underTest.getNullableByLogin("")).isNull();
     assertThat(underTest.getNullableByLogin("unknown")).isNull();
@@ -202,9 +200,7 @@ public class UserIndexTest {
       .setName(login.toUpperCase(Locale.ENGLISH))
       .setEmail(login + "@mail.com")
       .setActive(true)
-      .setScmAccounts(scmAccounts)
-      .setCreatedAt(DATE_1)
-      .setUpdatedAt(DATE_2);
+      .setScmAccounts(scmAccounts);
   }
 
   private static UserDoc newUser(String login, String email, List<String> scmAccounts) {
@@ -213,8 +209,6 @@ public class UserIndexTest {
       .setName(login.toUpperCase(Locale.ENGLISH))
       .setEmail(email)
       .setActive(true)
-      .setScmAccounts(scmAccounts)
-      .setCreatedAt(DATE_1)
-      .setUpdatedAt(DATE_2);
+      .setScmAccounts(scmAccounts);
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexerTest.java
index 66fb5c2ceae..3f4738b1aae 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexerTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexerTest.java
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.user.index;
 
+import java.util.HashSet;
 import java.util.List;
 import org.junit.Rule;
 import org.junit.Test;
@@ -26,6 +27,7 @@ import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserTesting;
 import org.sonar.server.es.EsTester;
 
 import static java.util.Arrays.asList;
@@ -45,53 +47,34 @@ public class UserIndexerTest {
 
   @Test
   public void index_nothing_on_startup() {
-    underTest.indexOnStartup(null);
+    underTest.indexOnStartup(new HashSet<>());
 
     assertThat(es.countDocuments(UserIndexDefinition.INDEX_TYPE_USER)).isEqualTo(0L);
   }
 
   @Test
-  public void index_everything_on_startup() {
-    db.users().insertUser(user -> user
-      .setLogin("user1")
-      .setName("User1")
-      .setEmail("user1@mail.com")
-      .setActive(true)
-      .setScmAccounts(asList("user_1", "u1"))
-      .setCreatedAt(1500000000000L)
-      .setUpdatedAt(1500000000000L));
-
-    underTest.indexOnStartup(null);
+  public void indexOnStartup_adds_all_users_to_index() {
+    UserDto user = db.users().insertUser(u -> u
+      .setScmAccounts(asList("user_1", "u1")));
 
-    List<UserDoc> docs = es.getDocuments(UserIndexDefinition.INDEX_TYPE_USER, UserDoc.class);
-    assertThat(docs).hasSize(1);
-    UserDoc doc = docs.get(0);
-    assertThat(doc.login()).isEqualTo("user1");
-    assertThat(doc.name()).isEqualTo("User1");
-    assertThat(doc.email()).isEqualTo("user1@mail.com");
-    assertThat(doc.active()).isTrue();
-    assertThat(doc.scmAccounts()).containsOnly("user_1", "u1");
-    assertThat(doc.createdAt()).isEqualTo(1500000000000L);
-    assertThat(doc.updatedAt()).isEqualTo(1500000000000L);
-  }
-
-  @Test
-  public void index_single_user_on_startup() {
-    UserDto user = db.users().insertUser();
-
-    underTest.indexOnStartup(null);
+    underTest.indexOnStartup(new HashSet<>());
 
     List<UserDoc> docs = es.getDocuments(UserIndexDefinition.INDEX_TYPE_USER, UserDoc.class);
     assertThat(docs).hasSize(1);
-    assertThat(docs).extracting(UserDoc::login).containsExactly(user.getLogin());
+    UserDoc doc = docs.get(0);
+    assertThat(doc.login()).isEqualTo(user.getLogin());
+    assertThat(doc.name()).isEqualTo(user.getName());
+    assertThat(doc.email()).isEqualTo(user.getEmail());
+    assertThat(doc.active()).isEqualTo(user.isActive());
+    assertThat(doc.scmAccounts()).isEqualTo(user.getScmAccountsAsList());
   }
 
   @Test
-  public void index_single_user() {
+  public void commitAndIndex_single_user() {
     UserDto user = db.users().insertUser();
     UserDto anotherUser = db.users().insertUser();
 
-    underTest.index(user.getLogin());
+    underTest.commitAndIndex(db.getSession(), user);
 
     List<UserDoc> docs = es.getDocuments(UserIndexDefinition.INDEX_TYPE_USER, UserDoc.class);
     assertThat(docs).hasSize(1);
@@ -101,14 +84,14 @@ public class UserIndexerTest {
   }
 
   @Test
-  public void index_several_users() {
-    UserDto user = db.users().insertUser();
-    UserDto anotherUser = db.users().insertUser();
+  public void commitAndIndex_multiple_users() {
+    UserDto user1 = db.getDbClient().userDao().insert(db.getSession(), UserTesting.newUserDto());
+    UserDto user2 = db.getDbClient().userDao().insert(db.getSession(), UserTesting.newUserDto());
 
-    underTest.index(asList(user.getLogin(), anotherUser.getLogin()));
+    underTest.commitAndIndex(db.getSession(), asList(user1, user2));
 
     List<UserDoc> docs = es.getDocuments(UserIndexDefinition.INDEX_TYPE_USER, UserDoc.class);
-    assertThat(docs).hasSize(2);
-    assertThat(docs).extracting(UserDoc::login).containsOnly(user.getLogin(), anotherUser.getLogin());
+    assertThat(docs).extracting(UserDoc::login).containsExactlyInAnyOrder(user1.getLogin(), user2.getLogin());
+    assertThat(db.countRowsOfTable(db.getSession(), "users")).isEqualTo(2);
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserResultSetIteratorTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserResultSetIteratorTest.java
deleted file mode 100644
index 852a8f75f45..00000000000
--- a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserResultSetIteratorTest.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.user.index;
-
-import com.google.common.collect.Maps;
-import java.util.Arrays;
-import java.util.Map;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.db.DbTester;
-import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.user.UserDto;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class UserResultSetIteratorTest {
-
-  @Rule
-  public DbTester db = DbTester.create();
-
-  @Test
-  public void iterator_over_users() {
-    UserDto userDto1 = db.users().insertUser(u -> u
-      .setName("User1")
-      .setLogin("user1")
-      .setEmail("user1@mail.com")
-      .setScmAccounts(Arrays.asList("user_1", "u1"))
-      .setCreatedAt(1_500_000_000_000L)
-      .setUpdatedAt(1_500_000_000_000L));
-    UserDto userDto2 = db.users().insertUser(u -> u
-      .setName("User2")
-      .setLogin("user2")
-      .setEmail("user2@mail.com")
-      .setScmAccounts(Arrays.asList("user,2", "user_2"))
-      .setCreatedAt(1_500_000_000_000L)
-      .setUpdatedAt(1_500_000_000_000L));
-    UserDto inactiveUser = db.users().insertUser(u -> u
-      .setName("User3")
-      .setLogin("user3")
-      .setEmail(null)
-      .setActive(false)
-      .setScmAccounts((String) null)
-      .setCreatedAt(1_500_000_000_000L)
-      .setUpdatedAt(1_550_000_000_000L));
-    OrganizationDto org1 = db.organizations().insertForUuid("ORG_1");
-    OrganizationDto org2 = db.organizations().insertForUuid("ORG_2");
-    db.organizations().addMember(org1, userDto1);
-    db.organizations().addMember(org1, userDto2);
-    db.organizations().addMember(org2, userDto1);
-
-    UserResultSetIterator it = UserResultSetIterator.create(db.getDbClient(), db.getSession(), null);
-    Map<String, UserDoc> usersByLogin = Maps.uniqueIndex(it, UserDoc::login);
-    it.close();
-
-    assertThat(usersByLogin).hasSize(3);
-
-    UserDoc user1 = usersByLogin.get("user1");
-    assertThat(user1.name()).isEqualTo("User1");
-    assertThat(user1.email()).isEqualTo("user1@mail.com");
-    assertThat(user1.active()).isTrue();
-    assertThat(user1.scmAccounts()).containsOnly("user_1", "u1");
-    assertThat(user1.createdAt()).isEqualTo(1_500_000_000_000L);
-    assertThat(user1.updatedAt()).isEqualTo(1_500_000_000_000L);
-    assertThat(user1.organizationUuids()).containsOnly("ORG_1", "ORG_2");
-
-    UserDoc user2 = usersByLogin.get("user2");
-    assertThat(user2.name()).isEqualTo("User2");
-    assertThat(user2.email()).isEqualTo("user2@mail.com");
-    assertThat(user2.active()).isTrue();
-    assertThat(user2.scmAccounts()).containsOnly("user,2", "user_2");
-    assertThat(user2.createdAt()).isEqualTo(1_500_000_000_000L);
-    assertThat(user2.updatedAt()).isEqualTo(1_500_000_000_000L);
-    assertThat(user2.organizationUuids()).containsOnly("ORG_1");
-
-    UserDoc user3 = usersByLogin.get("user3");
-    assertThat(user3.name()).isEqualTo("User3");
-    assertThat(user3.email()).isNull();
-    assertThat(user3.active()).isFalse();
-    assertThat(user3.scmAccounts()).isEmpty();
-    assertThat(user3.createdAt()).isEqualTo(1500000000000L);
-    assertThat(user3.updatedAt()).isEqualTo(1550000000000L);
-    assertThat(user3.organizationUuids()).isEmpty();
-  }
-}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java
index 6a09f91f7e8..3ff528cf1b6 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java
@@ -24,7 +24,6 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.BadRequestException;
@@ -60,7 +59,6 @@ public class ChangePasswordActionTest {
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
 
   private UserUpdater userUpdater = new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), new UserIndexer(db.getDbClient(), esTester.client()),
-    System2.INSTANCE,
     organizationFlags,
     TestDefaultOrganizationProvider.from(db),
     mock(OrganizationCreation.class),
@@ -159,13 +157,15 @@ public class ChangePasswordActionTest {
   public void fail_to_update_password_on_external_auth() throws Exception {
     userSessionRule.logIn().setSystemAdministrator();
 
-    userUpdater.create(db.getSession(), NewUser.builder()
+    NewUser newUser = NewUser.builder()
       .setEmail("john@email.com")
       .setLogin("john")
       .setName("John")
       .setScmAccounts(newArrayList("jn"))
       .setExternalIdentity(new ExternalIdentity("gihhub", "john"))
-      .build());
+      .build();
+    userUpdater.createAndCommit(db.getSession(), newUser, u -> {
+    });
 
     expectedException.expect(BadRequestException.class);
     tester.newPostRequest("api/users", "change_password")
@@ -175,12 +175,13 @@ public class ChangePasswordActionTest {
   }
 
   private void createUser() {
-    userUpdater.create(db.getSession(), NewUser.builder()
+    userUpdater.createAndCommit(db.getSession(), NewUser.builder()
       .setEmail("john@email.com")
       .setLogin("john")
       .setName("John")
       .setScmAccounts(newArrayList("jn"))
       .setPassword("Valar Dohaeris")
-      .build());
+      .build(), u -> {
+      });
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/CreateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/CreateActionTest.java
index d95b91cc0b2..704dc379de7 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/CreateActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/CreateActionTest.java
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.user.ws;
 
+import java.util.HashSet;
 import java.util.Optional;
 import org.junit.Before;
 import org.junit.Rule;
@@ -81,11 +82,10 @@ public class CreateActionTest {
   private GroupDto defaultGroupInDefaultOrg;
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
-
   private OrganizationCreation organizationCreation = mock(OrganizationCreation.class);
   private WsActionTester tester = new WsActionTester(new CreateAction(
     db.getDbClient(),
-    new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), userIndexer, system2, organizationFlags, defaultOrganizationProvider,
+    new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), userIndexer, organizationFlags, defaultOrganizationProvider,
       organizationCreation, new DefaultGroupFinder(db.getDbClient()), settings.asConfig()),
     userSessionRule));
 
@@ -234,7 +234,7 @@ public class CreateActionTest {
     logInAsSystemAdministrator();
 
     db.users().insertUser(newUserDto("john", "John", "john@email.com").setActive(false));
-    userIndexer.index("john");
+    userIndexer.indexOnStartup(new HashSet<>());
 
     call(CreateRequest.builder()
       .setLogin("john")
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java
index a0c8afd3350..9b8346c9de9 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java
@@ -334,14 +334,10 @@ public class DeactivateActionTest {
   }
 
   private UserDto insertUser(UserDto user) {
-    user
-      .setCreatedAt(system2.now())
-      .setUpdatedAt(system2.now());
     dbClient.userDao().insert(dbSession, user);
     dbClient.userTokenDao().insert(dbSession, newUserToken().setLogin(user.getLogin()));
     dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setUserId(user.getId()).setKey("foo").setValue("bar"));
-    dbSession.commit();
-    userIndexer.index(user.getLogin());
+    userIndexer.commitAndIndex(dbSession, user);
     return user;
   }
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java
index 6c44ded965a..2001d838249 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java
@@ -19,7 +19,7 @@
  */
 package org.sonar.server.user.ws;
 
-import com.google.common.collect.Lists;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
 import org.junit.Rule;
@@ -281,8 +281,7 @@ public class SearchActionTest {
   }
 
   private List<UserDto> injectUsers(int numberOfUsers) throws Exception {
-    List<UserDto> userDtos = Lists.newArrayList();
-    long createdAt = System.currentTimeMillis();
+    List<UserDto> userDtos = new ArrayList<>();
     GroupDto group1 = db.users().insertGroup(newGroupDto().setName("sonar-users"));
     GroupDto group2 = db.users().insertGroup(newGroupDto().setName("sonar-admins"));
     for (int index = 0; index < numberOfUsers; index++) {
@@ -293,15 +292,13 @@ public class SearchActionTest {
 
       UserDto userDto = dbClient.userDao().insert(dbSession, new UserDto()
         .setActive(true)
-        .setCreatedAt(createdAt)
         .setEmail(email)
         .setLogin(login)
         .setName(name)
         .setScmAccounts(scmAccounts)
         .setLocal(true)
         .setExternalIdentity(login)
-        .setExternalIdentityProvider("sonarqube")
-        .setUpdatedAt(createdAt));
+        .setExternalIdentityProvider("sonarqube"));
       userDtos.add(userDto);
 
       for (int tokenIndex = 0; tokenIndex < index; tokenIndex++) {
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/SkipOnboardingTutorialActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SkipOnboardingTutorialActionTest.java
index df97cd664de..620287f5534 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/SkipOnboardingTutorialActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SkipOnboardingTutorialActionTest.java
@@ -23,7 +23,6 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.api.utils.internal.TestSystem2;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.exceptions.UnauthorizedException;
@@ -35,9 +34,6 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 public class SkipOnboardingTutorialActionTest {
 
-  private final static long PAST = 100_000_000_000L;
-  private final static long NOW = 500_000_000_000L;
-
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
@@ -47,36 +43,31 @@ public class SkipOnboardingTutorialActionTest {
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
-  private TestSystem2 system2 = new TestSystem2().setNow(NOW);
-
-  private WsActionTester ws = new WsActionTester(new SkipOnboardingTutorialAction(userSession, db.getDbClient(), system2));
+  private WsActionTester ws = new WsActionTester(new SkipOnboardingTutorialAction(userSession, db.getDbClient()));
 
   @Test
   public void mark_user_as_onboarded() {
     UserDto user = db.users().insertUser(u -> u
-      .setOnboarded(false)
-      .setUpdatedAt(PAST));
+      .setOnboarded(false));
     userSession.logIn(user);
 
     call();
 
     UserDto userDto = selectUser(user.getLogin());
     assertThat(userDto.isOnboarded()).isEqualTo(true);
-    assertThat(userDto.getUpdatedAt()).isEqualTo(NOW);
   }
 
   @Test
   public void does_nothing_if_user_already_onboarded() {
     UserDto user = db.users().insertUser(u -> u
-      .setOnboarded(true)
-      .setUpdatedAt(PAST));
+      .setOnboarded(true));
     userSession.logIn(user);
 
     call();
 
     UserDto userDto = selectUser(user.getLogin());
     assertThat(userDto.isOnboarded()).isEqualTo(true);
-    assertThat(userDto.getUpdatedAt()).isEqualTo(PAST);
+    assertThat(userDto.getUpdatedAt()).isEqualTo(user.getUpdatedAt());
   }
 
   @Test
@@ -112,7 +103,6 @@ public class SkipOnboardingTutorialActionTest {
 
   @Test
   public void test_definition() {
-    WsActionTester ws = new WsActionTester(new SkipOnboardingTutorialAction(userSession, db.getDbClient(), system2));
     WebService.Action def = ws.getDef();
     assertThat(def.isPost()).isTrue();
     assertThat(def.isInternal()).isTrue();
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/UpdateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/UpdateActionTest.java
index 4747ce234a9..bce95d9b62c 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/UpdateActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/UpdateActionTest.java
@@ -75,7 +75,7 @@ public class UpdateActionTest {
     dbTester.users().insertDefaultGroup(dbTester.getDefaultOrganization(), "sonar-users");
     userIndexer = new UserIndexer(dbClient, esTester.client());
     tester = new WsTester(new UsersWs(new UpdateAction(
-      new UserUpdater(mock(NewUserNotifier.class), dbClient, userIndexer, system2, organizationFlags, defaultOrganizationProvider, ORGANIZATION_CREATION_NOT_USED_FOR_UPDATE,
+      new UserUpdater(mock(NewUserNotifier.class), dbClient, userIndexer, organizationFlags, defaultOrganizationProvider, ORGANIZATION_CREATION_NOT_USED_FOR_UPDATE,
         new DefaultGroupFinder(dbTester.getDbClient()), settings.asConfig()),
       userSessionRule,
       new UserJsonWriter(userSessionRule), dbClient)));
@@ -226,7 +226,6 @@ public class UpdateActionTest {
       .setExternalIdentity("jo")
       .setExternalIdentityProvider("sonarqube");
     dbClient.userDao().insert(session, userDto);
-    session.commit();
-    userIndexer.index(userDto.getLogin());
+    userIndexer.commitAndIndex(session, userDto);
   }
 }
-- 
cgit v1.2.3