Статьи

ReleaseCandidateTracker записывает развертывания в RavenDB

ReleaseCandidateTracker — это новое приложение на основе RavenDB от Szymon Pobiega. Я рассмотрел версию 5f7e42e0fb1dea70e53bace63f3e18d95d2a62dd. На данный момент я ничего не знаю об этом приложении, включая то, что именно оно означает, отслеживание релизов.

Я скачал код и запустил VS, в решении есть один проект, который уже хорош. Я решил рандомизировать свой подход к обзору и сначала проверить каталог моделей.

Вот как это выглядит:

образ

Это интересно по нескольким причинам. Во-первых, похоже, что он предназначен для учета всех развертываний в нескольких средах, и вы можете посмотреть историю каждого развертывания как на стороне среды, так и на стороне кандидата на выпуск.

Обратите внимание, что мы используем богатые модели, в которых есть коллекции. На самом деле, посмотрите на этот метод:

образ

Который вызывает этот метод:

образ

Вы знаете , что такое действительно интересное об этом?

Это не реляционная модель. На самом деле не нужно делать все эти звонки!

Затем мы переходим в папку «Инфраструктура», где у нас есть несколько результатов действий и материал для управления RavenDB. Вот как RCT использует RavenDB:

    public static class Database
    {
        private static IDocumentStore storeInstance;

        public static IDocumentStore Instance
        {
            get
            {
                if (storeInstance == null)
                {
                    throw new InvalidOperationException("Document store has not been initialized.");
                }
                return storeInstance;
            }
        }

        public static void Initialize()
        {
            var embeddableDocumentStore = new EmbeddableDocumentStore {DataDirectory = @"~\App_Data\Database"};
            embeddableDocumentStore.Initialize();
            storeInstance = embeddableDocumentStore;
        }
    }

Для этого используется встроенная база данных, что делает его очень простым в использовании. Просто нажмите F5 и вперед. На самом деле, если мы это сделаем, мы увидим полнофункциональный веб-сайт, что довольно здорово Улыбка.

Давайте перейдем к рассмотрению того, как мы управляем сессиями:

public class BaseController : Controller
{
    public IDocumentSession DocumentSession { get; private set; }
    public CandidateService CandidateService { get; private set; }
    public ScriptService ScriptService { get; private set; }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.IsChildAction)
        {
            return;
        }
        DocumentSession = Database.Instance.OpenSession();
        CandidateService = new CandidateService(DocumentSession);
        ScriptService = new ScriptService(DocumentSession);
        base.OnActionExecuting(filterContext);
    }
    
    protected override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (filterContext.IsChildAction)
        {
            return;
        }
        if(DocumentSession != null)
        {
            if (filterContext.Exception == null)
            {
                DocumentSession.SaveChanges();
            }
            DocumentSession.Dispose();
        }
        base.OnActionExecuted(filterContext);
    }
}

Все это обрабатывается внутри базового контроллера, и это очень похоже на то, как я это делаю в своих собственных приложениях.

Однако ScriptService и CandidateService кажутся странными, давайте немного их рассмотрим.

public class ScriptService
{
    private readonly IDocumentSession documentSession;

    public ScriptService(IDocumentSession documentSession)
    {
        this.documentSession = documentSession;
    }

    public void AttachScript(string versionNumber, Stream fileContents)
    {
        var metadata = new RavenJObject();
        documentSession.Advanced.DatabaseCommands.PutAttachment(versionNumber, null, fileContents, metadata);
    }

    public Stream GetScript(string versionNumber)
    {
        var attachment = documentSession.Advanced.DatabaseCommands.GetAttachment(versionNumber);
        return attachment != null 
            ? attachment.Data() 
            : null;
    }
}

Так что это использует вложение RavenDB для хранения вещей, я не совсем уверен, что еще, поэтому давайте отследить его.

Это используется так:

[HttpGet]
public ActionResult GetScript(string versionNumber)
{
    var candidate = CandidateService.FindOneByVersionNumber(versionNumber);
    var attachment = ScriptService.GetScript(versionNumber);
    if (attachment != null)
    {
        var result = new FileStreamResult(attachment, "text/plain");
        var version = candidate.VersionNumber;
        var product = candidate.ProductName;
        result.FileDownloadName = string.Format("deploy-{0}-{1}.ps1", product, version);
        return result;
    }
    return new HttpNotFoundResult("Deployment script missing.");
}

Поэтому я предполагаю, что эти сценарии являются сценариями развертывания для разных версий и загружаются при каждом новом выпуске.

Но посмотрите на CandidateService, он выглядит как традиционный сервис-оболочка RavenDB, и я говорил с ним несколько раз.

В частности, мне не нравится этот фрагмент кода:

public ReleaseCandidate FindOneByVersionNumber(string versionNumber)
{
    var result = documentSession.Query<ReleaseCandidate>()
        .Where(x => x.VersionNumber == versionNumber)
        .FirstOrDefault();
    if(result == null)
    {
        throw new ReleaseCandidateNotFoundException(versionNumber);
    }
    return result;
}

public void Store(ReleaseCandidate candidate)
{
    var existing = documentSession.Query<ReleaseCandidate>()
        .Where(x => x.VersionNumber == candidate.VersionNumber)
        .Any();
    if (existing)
    {
        throw new ReleaseCandidateAlreadyExistsException(candidate.VersionNumber);
    }
    documentSession.Store(candidate);
}

Если посмотреть на код, то похоже, что номер версии кандидата на выпуск является основным способом его поиска. Более того, во всей кодовой базе никогда не бывает случаев, когда мы загружаем документ по id.

Когда я вижу VersionNumber, я думаю о таких вещах, как «1.0.812.0», но я думаю, что в этом случае номер версии может также включать название продукта, «RavenDB-1.0.812.0», иначе вы не могли бы есть два продукта с одинаковой версией.

При этом приведенный выше код неверен, поскольку он не учитывает природу индексов RavenDB BASE. Вместо этого номер версии должен фактически быть идентификатором ReleaseCandidate. Таким образом, поскольку хранилище документов RavenDB полностью ACID, нам не нужно беспокоиться о времени обновления индекса, и мы можем загружать вещи очень эффективно.

Практически весь остальной код в CandidateService используется только в одном месте, и я не вижу в этом ценности.

Например, давайте посмотрим на это:

[HttpPost]
public ActionResult MarkAsDeployed(string versionNumber, string environment, bool success)
{
    CandidateService.MarkAsDeployed(versionNumber, environment, success);
    return new EmptyResult();
}

образ

Как видите, он просто загружает соответствующий кандидат на выпуск и вызывает для него метод MarkAsDeployed.

Вместо того чтобы делать это без необходимости, переадресации и предположения, что у нас есть VersionNumber в качестве идентификатора, я бы написал:

[HttpPost]
public ActionResult MarkAsDeployed(string versionNumber, string environment, bool success)
{
    var cadnidate = DocumentSession.Load<ReleaseCandidate>(versionNumber);
    if (cadnidate == null)
        throw new ReleaseCandidateNotFoundException(versionNumber);
    var env = DocumentSession.Load<DeploymentEnvironment>(environment);
    if (env == null)
        throw new InvalidOperationException(string.Format("Environment {0} not found", environment));

    cadnidate.MarkAsDeployed(success, env);
    return new EmptyResult();
}

Наконец, слово об обработке ошибок, это обрабатывается с помощью:

protected override void OnException(ExceptionContext filterContext)
{
    filterContext.Result = new ErrorResult(filterContext.Exception.Message);
    filterContext.ExceptionHandled = true;
}

public class ErrorResult : ActionResult
{
    private readonly string message;

    public ErrorResult(string message)
    {
        this.message = message;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        context.HttpContext.Response.Write(message);
        context.HttpContext.Response.StatusCode = 500;
    }
}

Сумасшедшая часть заключается в том, что OnException переопределяется только на некоторых контроллерах, а не на базовом контроллере, и даже хуже. Этот вид кода приводит к потере деталей ошибки.

Например, допустим, я получил исключение NullReferenceException. Этот код покорно расскажет мне все об этом, но не скажет мне, где это произошло .

Такие вещи делают отладку чрезвычайно сложной.