Primenu, Pingy – tai mano bandymas sukurti paslaugą, kuri kas tam tikrą laiką kreiptųsi į vartotojo nurodytą URL, taip neduodant IIS užmigti. Tuo pačiu metu bus renkama statistika apie serverio būseną, atsakos laikas ir pan. Pingy yra kuriamas specialiaiWindows Azure mokymosi tikslais.

Turinys

Pingy - #1 Architektūra
Pingy - #2 Windows Azure Table ir NoSQL mąstymas
Pingy - #3 PingItem ir PingItemRepository
Pingy - #4 PingItem repozitorijos atnaujinimas metodu GetItemsByPeriod
Pingy - #5 Windows Azure Queue ir PingTaskRepository
Pingy - #6 Windows Azure Queue korektiškas žinučių apdorojimas
Pingy - #7 Pinger serviso kūrimas
Pingy - #8 PingReportItem ir PingReportRepository

Praeitoje dalyje mes apibrėžėme pagrindinę PingItem esybę bei su ja susijusią repozitoriją. Šiandien aš parodysiu kaip viena, reliacinėse duomenų bazėse triviali, užklausa gali NoSQL sprendime priversti dubliuoti duomenis.

Metodas GetItemsByPeriod

Priminsiu, praeitame straipsnyje mes turėjome tokį PingItemRepository interfeisą:

public interface IPingItemRepository
{
    IEnumerable<PingItem> GetClientItems(string client);
    bool Exists(PingItem item);
    void AddItem(PingItem item);
    void RemoveItem(PingItem item);
    void RemoveClientItems(string client);
}

Prisiminkime, kad mums reikia ne tik saugoti informaciją, bet ir kreiptis į nurodytus URL. Pas mus tas mechanizmas bus realizuotas pakankamai paprastai: kiekvienai PingPeriod išvardijimo reikšmei bus sukurta po giją, kuri kas nurodytą laiką užklaus į kokius URL reikia kreiptis ir patalpins atitinkamas užduotis į eilę. Už tai atsakys servisas Ping Scheduler. Specialiai jam mums reikia atnaujinti IPingItemRepository metodu GetItemsByPeriod:

public interface IPingItemRepository
{
    IEnumerable<PingItem> GetItemsByPeriod(PingPeriod period);
    IEnumerable<PingItem> GetClientItems(string client);
    bool Exists(PingItem item);
    void AddItem(PingItem item);
    void RemoveItem(PingItem item);
    void RemoveClientItems(string client);
}

NoSQL ypatumai

Iš pirmo žvilgsnio, metodo realizacija gali pasirodyti triviali ir atrodyti taip:

public IEnumerable<PingItem> GetItemsByPeriod(PingPeriod period)
{
    return CreateContext().CreateQuery<PingEntity>(pingEntityTable)
                          .Where(e => e.Period == (int)period)
                          .AsTableServiceQuery()
                          .Execute()
                          .Select(e => e.ToPingItem());
}

Pagrindinė šios realizacijos problema – ypatingai lėta greitaveika ir ribotas plečiamumas. Prisiminkime, duomenys Windows Azure Table saugykloje gali būti saugomi skirtinguose fiziniuose įrenginiuose. Tai reiškia, kad išreikštinai nenurodant PartitionKey bus ieškoma visose saugyklose! Be abejo tai yra labai neoptimalu, todėl reikia optimizuoti užklausą – saugoti PingItem esybes sugrupuotas pagal esamus periodus. Vienintelis būdas pasiekti tai su Windows Azure Table –  dubliuoti duomenis apibrėžiant naują lentelę ir jai skirtą modelį PeriodicPingEntity:

public class PeriodicPingEntity : TableServiceEntity
{
    public string Client { get; set; }
    public string Url { get; set; }
}

Šiame modelyje PingPeriod yra naudojamas kaip PartitionKey. Papildykime mūsų pagalbinę esybių konvertavimui skirtą klasę PingItemConverter:

public static class PingItemConverter
{
    public static PingItem ToPingItem(this PingEntity item)
    {
        return new PingItem
        {
            Client = item.PartitionKey,
            Url = item.Url,
            Period = (PingPeriod)item.Period,
        };
    }

    public static PingEntity ToPingEntity(this PingItem item)
    {
        return new PingEntity
        {
            PartitionKey = item.Client,
            RowKey = String.Format("{0}_{1}", item.Client, item.Url.SanitizeUrl()),
            Url = item.Url,
            Period = (int)item.Period,
        };
    }

    public static PingItem ToPingItem(this PeriodicPingEntity item)
    {
        return new PingItem
        {
            Client = item.Client,
            Url = item.Url,
            Period = (PingPeriod)Int32.Parse(item.PartitionKey),
        };
    }

    public static PeriodicPingEntity ToPeriodicPingEntity(this PingItem item)
    {
        return new PeriodicPingEntity
        {
            PartitionKey = ((int)item.Period).ToString(),
            RowKey = String.Format("{0}_{1}", item.Client, item.Url.SanitizeUrl()),
            Client = item.Client,
            Url = item.Url,
        };
    }
}

PingItemRepository atnaujinimas

Kadangi PeriodicPingEntity modeliai iš esmės tiesiogiai susiję su PingEntity, mums reikės ne tik įgyvendinti GetItemsByPeriod metodą, bet ir papildyti kitus metodus operacijomis su PeriodicPingEntity. Pilna (ir galutinė) PingItemRepository realizacija atrodo taip:

public class PingItemRepository : IPingItemRepository
{
    private readonly CloudStorageAccount account;
    private readonly string pingEntityTable = typeof(PingEntity).Name;
    private readonly string periodicPingEntityTable = typeof(PeriodicPingEntity).Name;

    public PingItemRepository(CloudStorageAccount account)
    {
        this.account = account;

        var client = account.CreateCloudTableClient();
        client.CreateTableIfNotExist(pingEntityTable);
        client.CreateTableIfNotExist(periodicPingEntityTable);
    }

    public IEnumerable<PingItem> GetItemsByPeriod(PingPeriod period)
    {
        return CreateContext().CreateQuery<PeriodicPingEntity>(periodicPingEntityTable)
                              .Where(e => e.PartitionKey == ((int)period).ToString())
                              .AsTableServiceQuery()
                              .Execute()
                              .Select(e => e.ToPingItem());
    }

    public IEnumerable<PingItem> GetClientItems(string client)
    {
        return CreateContext().CreateQuery<PingEntity>(pingEntityTable)
                              .Where(e => e.PartitionKey == client)
                              .AsTableServiceQuery()
                              .Execute()
                              .Select(e => e.ToPingItem());
    }

    public bool Exists(PingItem item)
    {
        var entity = item.ToPingEntity();
        return CreateContext().CreateQuery<PingEntity>(pingEntityTable)
                              .Where(e => e.PartitionKey == entity.PartitionKey)
                              .Where(e => e.RowKey == entity.RowKey)
                              .FirstOrDefault() != null;
    }

    public void AddItem(PingItem item)
    {
        var context = CreateContext();

        var pingEntity = item.ToPingEntity();
        context.AddObject(pingEntityTable, pingEntity);
        context.SaveChanges();

        var periodicPingEntity = item.ToPeriodicPingEntity();
        context.AddObject(periodicPingEntityTable, periodicPingEntity);
        context.SaveChanges();
    }

    public void RemoveItem(PingItem item)
    {
        var context = CreateContext();

        var pingEntity = item.ToPingEntity();
        context.AttachTo(pingEntityTable, pingEntity, "*");
        context.DeleteObject(pingEntity);
        context.SaveChanges();

        var periodicPingEntity = item.ToPeriodicPingEntity();
        context.AttachTo(periodicPingEntityTable, periodicPingEntity, "*");
        context.DeleteObject(periodicPingEntity);
        context.SaveChanges();
    }

    public void RemoveClientItems(string client)
    {
        var context = CreateContext();

        var pingEntities = context.CreateQuery<PingEntity>(pingEntityTable)
                                  .Where(e => e.PartitionKey == client)
                                  .AsTableServiceQuery()
                                  .Execute()
                                  .ToList();

        foreach (var entity in pingEntities)
        {
            context.DeleteObject(entity);
        }
        context.SaveChanges();

        foreach (var pingEntity in pingEntities)
        {
            var periodicPingEntity = pingEntity.ToPingItem().ToPeriodicPingEntity();
            context.AttachTo(periodicPingEntityTable, periodicPingEntity, "*");
            context.DeleteObject(periodicPingEntity);
        }
        context.SaveChanges();
    }

    protected TableServiceContext CreateContext()
    {
        return new TableServiceContext(account.TableEndpoint.ToString(), account.Credentials);
    }
}

Kitoje dalyje…

Kaip matote, programuojant debesims ir mąstant apie plečiamumą, kartais iš pirmo žvilgsnio paprastas uždavinys gali pavirsti visai netrivialiu. Kitoje dalyje mes pereisime prie kreipimosi į URL užduočių formavimo, o tiksliau prie PingTaskRepository klasės ir Windows Azure Queue panaudojimo.

Rodyk draugams