Browse Source

Merge pull request #5 from englishtown/feature/retry-on-error

MirrorRepositoryHook - the mirror command is attempted up to 5 times wit...
pull/6/head
Adrian 12 years ago
parent
commit
83cd6530f9
  1. 60
      src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java
  2. 8
      src/main/resources/atlassian-plugin.xml
  3. 72
      src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java

60
src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java

@ -19,20 +19,29 @@ import javax.annotation.Nonnull;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Collection; import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator { public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator {
static final String SETTING_MIRROR_REPO_URL = "mirrorRepoUrl"; static final String SETTING_MIRROR_REPO_URL = "mirrorRepoUrl";
static final String SETTING_USERNAME = "username"; static final String SETTING_USERNAME = "username";
static final String SETTING_PASSWORD = "password"; static final String SETTING_PASSWORD = "password";
static final int MAX_ATTEMPTS = 5;
private final GitScm gitScm; private final GitScm gitScm;
private final I18nService i18nService; private final I18nService i18nService;
private final ScheduledExecutorService executor;
private static final Logger logger = LoggerFactory.getLogger(MirrorRepositoryHook.class); private static final Logger logger = LoggerFactory.getLogger(MirrorRepositoryHook.class);
public MirrorRepositoryHook(GitScm gitScm, I18nService i18nService) { public MirrorRepositoryHook(
GitScm gitScm,
I18nService i18nService,
ScheduledExecutorService executor) {
this.gitScm = gitScm; this.gitScm = gitScm;
this.i18nService = i18nService; this.i18nService = i18nService;
this.executor = executor;
} }
/** /**
@ -51,7 +60,6 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
@Nonnull RepositoryHookContext context, @Nonnull RepositoryHookContext context,
@Nonnull Collection<RefChange> refChanges) { @Nonnull Collection<RefChange> refChanges) {
try {
logger.debug("MirrorRepositoryHook: postReceive started."); logger.debug("MirrorRepositoryHook: postReceive started.");
Settings settings = context.getSettings(); Settings settings = context.getSettings();
@ -59,9 +67,24 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
String username = settings.getString(SETTING_USERNAME); String username = settings.getString(SETTING_USERNAME);
String password = settings.getString(SETTING_PASSWORD); String password = settings.getString(SETTING_PASSWORD);
URI authenticatedUrl = getAuthenticatedUrl(mirrorRepoUrl, username, password); runMirrorCommand(mirrorRepoUrl, username, password, context.getRepository());
GitScmCommandBuilder builder = gitScm.getCommandBuilderFactory().builder(context.getRepository());
CommandExitHandler exitHandler = new GitCommandExitHandler(i18nService, context.getRepository()); }
void runMirrorCommand(String mirrorRepoUrl, String username, final String password, final Repository repository) {
try {
final URI authenticatedUrl = getAuthenticatedUrl(mirrorRepoUrl, username, password);
executor.submit(new Callable<Void>() {
int attempts = 0;
@Override
public Void call() throws Exception {
try {
GitScmCommandBuilder builder = gitScm.getCommandBuilderFactory().builder(repository);
CommandExitHandler exitHandler = new GitCommandExitHandler(i18nService, repository);
PasswordHandler passwordHandler = new PasswordHandler(password, exitHandler); PasswordHandler passwordHandler = new PasswordHandler(password, exitHandler);
// Call push command with the mirror flag set // Call push command with the mirror flag set
@ -74,13 +97,26 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
.build(passwordHandler) .build(passwordHandler)
.call(); .call();
builder.defaultExitHandler();
logger.debug("MirrorRepositoryHook: postReceive completed with result '{}'.", result); logger.debug("MirrorRepositoryHook: postReceive completed with result '{}'.", result);
} catch (Exception e) { } catch (Exception e) {
logger.error("MirrorRepositoryHook: Error running mirror hook", e); if (++attempts > MAX_ATTEMPTS) {
logger.error("Failed to mirror repository " + repository.getName() + " after " + --attempts
+ " attempts.", e);
} else {
logger.warn("Failed to mirror repository " + repository.getName() + ", " +
"retrying in 1 minute.");
executor.schedule(this, 1, TimeUnit.MINUTES);
}
}
return null;
} }
});
} catch (Exception e) {
logger.error("MirrorRepositoryHook: Error running mirror hook", e);
}
} }
URI getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) throws URISyntaxException { URI getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) throws URISyntaxException {
@ -129,16 +165,22 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
} }
} }
if (settings.getString(SETTING_USERNAME, "").isEmpty()) { String username = settings.getString(SETTING_USERNAME, "");
if (username.isEmpty()) {
count++; count++;
errors.addFieldError(SETTING_USERNAME, "The username is required."); errors.addFieldError(SETTING_USERNAME, "The username is required.");
} }
if (settings.getString(SETTING_PASSWORD, "").isEmpty()) { String password = settings.getString(SETTING_PASSWORD, "");
if (password.isEmpty()) {
count++; count++;
errors.addFieldError(SETTING_PASSWORD, "The password is required."); errors.addFieldError(SETTING_PASSWORD, "The password is required.");
} }
// If no errors, run the mirror command
if (count == 0) {
runMirrorCommand(mirrorRepoUrl, username, password, repository);
}
logger.debug("MirrorRepositoryHook: validate completed with {} error(s).", count); logger.debug("MirrorRepositoryHook: validate completed with {} error(s).", count);
} catch (Exception e) { } catch (Exception e) {

8
src/main/resources/atlassian-plugin.xml

@ -8,15 +8,13 @@
</plugin-info> </plugin-info>
<!-- Components that are injected --> <!-- Components that are injected -->
<component-import key="gitScm" interface="com.atlassian.stash.scm.git.GitScm" /> <component-import key="gitScm" interface="com.atlassian.stash.scm.git.GitScm"/>
<component-import key="i18nService" interface="com.atlassian.stash.i18n.I18nService" /> <component-import key="i18nService" interface="com.atlassian.stash.i18n.I18nService"/>
<component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties"/>
<!-- add our i18n resource --> <!-- add our i18n resource -->
<resource type="i18n" name="i18n" location="stash-hook-mirror"/> <resource type="i18n" name="i18n" location="stash-hook-mirror"/>
<!-- import from the product container -->
<component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties"/>
<repository-hook name="Mirror Repository Hook" i18n-name-key="mirror-repository-hook.name" <repository-hook name="Mirror Repository Hook" i18n-name-key="mirror-repository-hook.name"
key="mirror-repository-hook" class="com.englishtown.stash.hook.MirrorRepositoryHook"> key="mirror-repository-hook" class="com.englishtown.stash.hook.MirrorRepositoryHook">
<description key="mirror-repository-hook.description">Mirror Repository Hook</description> <description key="mirror-repository-hook.description">Mirror Repository Hook</description>

72
src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java

@ -15,10 +15,18 @@ import com.atlassian.stash.setting.Settings;
import com.atlassian.stash.setting.SettingsValidationErrors; import com.atlassian.stash.setting.SettingsValidationErrors;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -26,22 +34,28 @@ import static org.mockito.Mockito.*;
/** /**
* Unit tests for {@link MirrorRepositoryHook} * Unit tests for {@link MirrorRepositoryHook}
*/ */
@RunWith(MockitoJUnitRunner.class)
public class MirrorRepositoryHookTest { public class MirrorRepositoryHookTest {
private MirrorRepositoryHook hook; private MirrorRepositoryHook hook;
private GitScmCommandBuilder builder; private GitScmCommandBuilder builder;
@Mock
private GitCommand<String> cmd; private GitCommand<String> cmd;
@Mock
private ScheduledExecutorService executor;
private final String mirrorRepoUrl = "https://stash-mirror.englishtown.com/scm/test/test.git"; private final String mirrorRepoUrl = "https://stash-mirror.englishtown.com/scm/test/test.git";
private final String username = "test-user"; private final String username = "test-user";
private final String password = "test-password"; private final String password = "test-password";
private final String repository = "https://test-user:test-password@stash-mirror.englishtown.com/scm/test/test.git"; private final String repository = "https://test-user:test-password@stash-mirror.englishtown.com/scm/test/test.git";
@SuppressWarnings("unchecked") @SuppressWarnings("UnusedDeclaration")
@Captor
ArgumentCaptor<Callable<Void>> argumentCaptor;
@Before @Before
public void setup() { public void setup() {
cmd = mock(GitCommand.class);
builder = mock(GitScmCommandBuilder.class); builder = mock(GitScmCommandBuilder.class);
when(builder.command(anyString())).thenReturn(builder); when(builder.command(anyString())).thenReturn(builder);
when(builder.argument(anyString())).thenReturn(builder); when(builder.argument(anyString())).thenReturn(builder);
@ -55,7 +69,7 @@ public class MirrorRepositoryHookTest {
GitScm gitScm = mock(GitScm.class); GitScm gitScm = mock(GitScm.class);
when(gitScm.getCommandBuilderFactory()).thenReturn(builderFactory); when(gitScm.getCommandBuilderFactory()).thenReturn(builderFactory);
hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class)); hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor);
} }
@ -67,12 +81,62 @@ public class MirrorRepositoryHookTest {
when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME))).thenReturn(username); when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME))).thenReturn(username);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD))).thenReturn(password); when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD))).thenReturn(password);
Repository repo = mock(Repository.class);
when(repo.getName()).thenReturn("test");
RepositoryHookContext context = mock(RepositoryHookContext.class); RepositoryHookContext context = mock(RepositoryHookContext.class);
when(context.getSettings()).thenReturn(settings); when(context.getSettings()).thenReturn(settings);
when(context.getRepository()).thenReturn(repo);
Collection<RefChange> refChanges = new ArrayList<RefChange>(); Collection<RefChange> refChanges = new ArrayList<RefChange>();
hook.postReceive(context, refChanges); hook.postReceive(context, refChanges);
verifyExecutor();
}
@Test
public void testRunMirrorCommand_Retries() throws Exception {
GitScm gitScm = mock(GitScm.class);
when(gitScm.getCommandBuilderFactory()).thenThrow(new RuntimeException("Intentional unit test exception"));
MirrorRepositoryHook hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor);
hook.runMirrorCommand(mirrorRepoUrl, username, password, mock(Repository.class));
verify(executor).submit(argumentCaptor.capture());
Callable<Void> callable = argumentCaptor.getValue();
callable.call();
verify(executor, times(1)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class));
callable = argumentCaptor.getValue();
callable.call();
verify(executor, times(2)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class));
callable = argumentCaptor.getValue();
callable.call();
verify(executor, times(3)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class));
callable = argumentCaptor.getValue();
callable.call();
verify(executor, times(4)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class));
callable = argumentCaptor.getValue();
callable.call();
verify(executor, times(5)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class));
callable = argumentCaptor.getValue();
callable.call();
// Make sure it is only called 5 times
verify(executor, times(5)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class));
}
private void verifyExecutor() throws Exception {
verify(executor).submit(argumentCaptor.capture());
Callable<Void> callable = argumentCaptor.getValue();
callable.call();
verify(builder, times(1)).command(eq("push")); verify(builder, times(1)).command(eq("push"));
verify(builder, times(1)).argument(eq("--mirror")); verify(builder, times(1)).argument(eq("--mirror"));
verify(builder, times(1)).argument(eq(repository)); verify(builder, times(1)).argument(eq(repository));
@ -96,7 +160,7 @@ public class MirrorRepositoryHookTest {
Settings settings = mock(Settings.class); Settings settings = mock(Settings.class);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), eq(""))) when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), eq("")))
.thenThrow(new RuntimeException()) .thenThrow(new RuntimeException("Intentional unit test exception"))
.thenReturn("") .thenReturn("")
.thenReturn("invalid uri") .thenReturn("invalid uri")
.thenReturn("http://should-not:have-user@stash-mirror.englishtown.com/scm/test/test.git") .thenReturn("http://should-not:have-user@stash-mirror.englishtown.com/scm/test/test.git")

Loading…
Cancel
Save