daron yöndem | Microsoft Regional Director | Silverlight MVP
Microsoft Regional Director | Nokia Developer Champion | Azure MVP
Bu yazıyı yazmam 110 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 4 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Eğer IoT taraflarıyla uğraştıysanız Azure Event Hub ile de karşılaşmış olmanız olası. Ben bu yazıda Event Hub'ın detaylarını ileriki bir yazıya bırarak Event Hub deneyimizin olduğunu varsayarak Azure Functions entegrasyonundan bahsedeceğim. Azure Functions ile beraber Event Hub ile entegre olabilmek adına hem eventHubTrigger geliyor hem de input ve output binding özellikleri geliyor.

Test ortamımız

Test ortamımız epey basit. eventHubTrigger'ı test edebilmek adına içine eventData gelen bir Hub'a ihtiyacımız var. Bunun için Azure tarafında bir darontest adında Event Hub yaratıp içinde de darontesthub adında bir hub yarattım ben. Sonrasında basit bir uygulama ile hub'a eventData gönderdim.

[uygulama.cs]

string eventHubName = "darontesthub";
string connectionString = "{BURAYA SİZİN CONNECTION STRING GELECEK}";

var eventHubClient = EventHubClient.CreateFromConnectionString(connectionString, eventHubName);

var message = Guid.NewGuid().ToString();
eventHubClient.Send(new EventData(Encoding.UTF8.GetBytes(message)) { PartitionKey = "Test" });

Yukarıdaki kodu çalıştırdığımızda basit bir şekilde için GUID olan bir mesajı Test adında bir Partition'a atıyoruz.

Event Hub Trigger Binding

Event Hub Trigger'ımızı tanımlarken Azure'daki ortamın connection stringini kullanmamız gerekecek. Bunun için appsettings.json'ı kullanabiliriz.

[appsettings.json]

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AzureWebJobsDashboard": "UseDevelopmentStorage=true",
    "AzureEventHubConnectionString": "{BURAYA SİZİN CONNECTION STRING GELECEK}"
  }
}

Yukarıdaki şekilde Event Hub connection stringini Azure portalından alıp appsettings.json'a yazdıktan sonra artık klasik trigger tanımımızı yapabiliriz. Bunun için de functions.json'a geçiyoruz.

[function.json]

{
  "bindings": [
    {
      "type": "eventHubTrigger",
      "name": "telemetriGelen",
      "direction": "in",
      "path": "darontesthub",
      "consumerGroup": "$Default",
      "connection": "AzureEventHubConnectionString"
    },
    {
      "name": "cikanNesneler",
      "type": "table",
      "tableName": "Cikanlar",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ],
  "disabled": false
}

Ben daha önceki bir yazıda yaptığımız Table Trigger'dan yola çıkarak devam edelim dedim :) O nedenle hızlıca yukarıya bir Table Output Binding koydum. Amacımız Event Hub'a eventData geldiğinde bunu yakalayıp Table'a kaydetmek. Tabi ki arada siz farklı işlemler de yapabilirsiniz. Ben örnek için "al gülüm, ver gülüm" yapıyorum :)

Yukarıdaki function.json'a bakarsanız üst kısımda eventHubTrigger'ı bulabilirsiniz. Binding'imize type olarak eventHubTrigger dedikten sonra Azure Function'da metod imzasında bind edilecek parametrenin adını da name değerine veriyoruz. Dışarıdan içeriye data geleceği için direction in olarak kalıyor. path dediğimiz ise kullanacağımız hub'ın adı. consumerGroup kısmını aslında boş bıraksam da Event Hub'daki $Default'a yönlenirdi. Ben yine de boş geçmemek adına yazdım, fakat siz tabi ki istediğiniz Consumer Group adını yazabilirsiniz. Azure Event Hub'a özel bir tavsiye olarak Event Hub'a bağlı functionlarınızı çok şişirmemenizi tavsiye ederim. Onun yerine birden çok Function yazıp farklı Consumer Group'lar kullanmanız daha performanslı olacaktır. Azure Functions SDK arka planda EventProcessorHost kullanıyor ve EventProcessorHost Consumer Group'larda Parition başına tek reader kullanır. EventProcessorHost kendi içerisinde ölçeklenebilse de işinizi bölebiliyorsanız ayrı götürmen daha da hızlandıracaktır. Son olarak bir önceki adımda appsettings.json'a eklediğimiz connection stringi burada da connection olarak veriyoruz.

[run.csx]

 #r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Table;
public static void Run(string telemetriGelen, ICollector<OrnekObje> cikanNesneler, TraceWriter log)
{
    OrnekObje gidenObje = new OrnekObje();
    gidenObje.PartitionKey = System.Guid.NewGuid().ToString();
    gidenObje.RowKey = System.Guid.NewGuid().ToString();
    gidenObje.Metin = $"{telemetriGelen} alındı";
    cikanNesneler.Add(gidenObje);
}

public class OrnekObje
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Metin { get; set; }
}

Yukarıdaki function'da telemetriGelen string tipinden bir değişken ve bize doğrudan Event Hub'a gönderilen veriyi getiriyor. Biz bu örnekte Random birer ParitionKey ve RowKey vererek doğrudan Table Services'a atıyoruz. Eğer eventData ile ilgili daha çok veriye ulaşmak isterseniz **Service Bus SDK'ine başvurmanız gerekecek.

Table Services'a attığımız Event Hub verisi.

[project.json]

{
  "frameworks": {
    "net46":{
      "dependencies": {
        "WindowsAzure.ServiceBus" : "3.4.3"
      }
    }
  }
}

İlk aşamada yukarıdaki gibi ServiceBus nuget paketini projeye ekleyin. Bunu yaptıktan sonra projeyi çalıştırdığınızda Azure Functions Runtime function'ı çalıştırmadan önce nuget restore da yapacak. Sonrasında doğrudan using ile kütüphaneyi kullanabiliriz. Gelin onu da output binding'e bakarken yapalım.

Output Binding

Output binding için function.json dosyasındaki Table Binding'i kaldırarak yerine tahmin edebileceğiniz üzere bir Event Hub Binding koyuyoruz. **Event Hub Trigger'dan farklı olarak direction out olacak ve Consumer Group olmayacak.

[function.json]

{
  "bindings": [
    {
      "type": "eventHubTrigger",
      "name": "telemetriGelen",
      "direction": "in",
      "path": "darontesthub",
      "consumerGroup": "$Default",
      "connection": "AzureEventHubConnectionString"
    },
    {
      "type": "eventHub",
      "name": "telemetriGiden",
      "path": "darontesthub",
      "connection": "AzureEventHubConnectionString2",
      "direction": "out"
    }
  ],
  "disabled": false
}

Arada ben ikinci bir Hub daha yaratıp onun da connection stringi ekledim appsettings.json'a. Böylece bir hub'dan alıp diğerine aktarıyor olacağız.

[run.csx]

using Microsoft.ServiceBus.Messaging;

public static void Run(EventData telemetriGelen, ICollector<string> telemetriGiden, TraceWriter log)
{
    telemetriGiden.Add("Mesaj 1 " + telemetriGelen);
    telemetriGiden.Add("Mesaj 2 " + telemetriGelen);
}

Gördüğünüz üzere Event Hub Trigger'ın bindinginde nesne tipi olarak EventData kullandık. Böylece artık eventData'nın sadece body değil tüm bilgilerine ulaşabiliriz. Ayrıca output binding'de de ICollector kullanarak geriye birden çok event gönderiyoruz. Burada ICollector yerine isterseniz ICollector de kullanabilirsiniz.

Ek ayarlar

[Host.json]

{
    "eventHub": {
      "maxBatchSize": 64,
      "prefetchCount": 256
    },
}

Yukarıdaki ayarları varsayılan değerleri ile yazdım. maxBatchSize bir defada event hub'dan alınacak mesaj sayısını belirliyor. PrefetchCount da tahmin edebileceğiniz üzere aslında Azure Functions SDK'in arkaplanda kullandığı EventProcessorHost ile ilgili. Toplamda ne kadar mesajın işlenme öncesinde belleğe alınması gerektiğini belirtiyor. Azure Functions SDK mesaj göndermek için EventHubClient ve dinlemek için EventProcessorHost kullanıyor.

Bu yazıyı yazmam 87 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 6 dakikada, göz gezdirerek ortalama 2 dakikada okuyabilirsiniz ;)

Azure Functions ile Azure Storage'daki Table Services arasında bir TableTrigger yok. Fakat QueueTrigger veya TimerTrigger gibi tetkikleyicilerle beraber kullanarak TableBinding ile Table Services'dan veri alıp gönderebilirsiniz. Tüm bunlardan önce eğer "Table Services da neyin nesi?" diyorsanız :) Giriş, gelişme ve sonuç diyerek tüm bunların öncesinde Table Services yazılarımı okumanızı tavsiye ederim.

Input Binding

Table Binding Test Ortamımız

İlk olarak yukarıdaki şekilde local storage ortamını hazırlayalım. Örneğimizde ornekkuyruk adında bir kuyruk kullanacağız. Bu kuyruğa atacağımız ilçe ismine göre Nesneler adındaki Table'dan PartitionKey şehir ve RowKey de ilçe olacak şekilde doğru ilçeyi bulacağız. Sonrasında eldeki veriden birkaç absürd değişiklik yapıp :) Cikanlar tablosuna da ayrı bir binding ile veri atmayı deneriz.

[function.json]

{
  "bindings": [
    {
      "queueName": "ornekkuyruk",
      "connection": "AzureWebJobsStorage",
      "name": "kuyruk",
      "type": "queueTrigger",
      "direction": "in"
    },
    {
      "name": "gelenNesne",
      "type": "table",
      "tableName": "Nesneler",
      "partitionKey": "Istanbul",
      "rowKey": "{queueTrigger}",
      "connection": "AzureWebJobsStorage",
      "direction": "in"
    }
  ],
  "disabled": false
}

İlk aşamada function.json dosyamız yukarıdaki gibi olacak. Üst kısımda basit bir QueueTrigger tanımı yapıyoruz. QueueTrigger detaylarını burada bulabilirsiniz. Böylece birazdan göreceğimiz Azure Function'a bu kuyruğa gelen görevlerin kuyruk adlı bir nesne ile gönderileceğini biliyoruz.

Sonraki adımda bir Table Binding oluşturuyoruz. Bunun için typetable olan bir nesne koymamız gerekiyor. Bu JSON nesnesinin name özelliği yine bizim Azure Function içerisine parametre olarak gönderilecek verinin adını tanımlıyor. Tabi Table Storage'dan bahsettiğimiz için bir Table'ı hedeflememiz gerek. Onun için de tableName'e ilk adımda yarattığımız test table'larından Nesneler adındaki table'ı veriyoruz. connection yine appsettings.jsondaki storage account connection stringi gösteriyor, direction ise içeriye data aktaracağımız için in şeklinde tanımlanmış.

Farkındaysanız paritionKey ve rowKey özelliklerini atladım. Onlara ayrıca odaklanmak istedim. Table Services'da bildiğiniz üzere her nesne bir ParitionKey ve RowKey sahibi olmak zorunda. Detaylarını yazının başında linkini verdiğim yazılardan inceleyebilirsiniz. Bu noktada Table Services'ı bildiğinizi varsayıyorum. TableBinding yaparken Table'dan bir data bulmaya çalışacağız. Bu datanın bir şekilde ParitionKey ve RowKey ile aranması gerekiyor. Bizim örneğimizde ParitionKey olarak Istanbul değerini vererek aslında bu Function'da kullanılacak binding'in her zaman Istanbul Parition'ına gideceğini söylemiş oluyoruz. Siz tabi ki ihtiyacınıza göre farklı bir tasarım yapabilirsiniz. rowKey özelliğine baktığınızda ise {queueTrigger} diye bir placeholder göreceksiniz. Aslında bu da bir binding. Bu keyword'ü kullanarak QueueTrigger'dan gelen değeri TableBinding'in içindeki RowKey'e bind etmiş oluyoruz. Yani eğer kuyruktan için "Sisli" yazan bir mesaj gelirse bizim Table Binding ile Istanbul PartitionKey'li ve "Sisli" yazan RowKey'li obje dönecek. Eğer kuyruktan gelen veriyi bu kadar rahat bir şekilde ParitionKey veya RowKey'e bind edemiyorsanız yazının sonunda bu filtrelemeyi Azure Function'ın kendi içinde nasıl yapabileceğinizden de bahsedeceğim.

[run.cxs]

public static void Run(string kuyruk, OrnekObje gelenNesne, TraceWriter log)
{
    log.Info($"C# Queue trigger'dan gelen: {kuyruk}");
    log.Info($"Table Services'dan gelen metin: {gelenNesne.Metin}");
    gelenNesne.Metin += "OK";
}

public class OrnekObje
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Metin { get; set; }
}

Yukarıdaki koda baktığınızda birincisi alt kısımda binding için kullanacağımız OrnekObje adındaki POCO'yu görüyorsunuz. Nesne Table Services'dan geleceği için ParitionKey ve RowKey özellikleri mecburi.

Esas Azure Function'a baktığımızda ise hem kuyruktan gelen verinin kuyruk değişkeni ile geldiğini hem de Table Services'dan Istanbul ParitionKey'li ve kuyruktan gelen değere uyan RowKeyli nesnenin gelenNesne adı ve OrnekObje tipi ile geldiğini görebilirsiniz.

Daha da fazlası, gelenNesnenin Metin adındaki Property'sinde ufak bir değişiklik de yapıyoruz. Bu değişikliği ayrıca commit etmeniz vs gerekmiyor. Function bittiği anda bu değişiklik doğrudan Table Services'daki Entity'ye yansıyacak.

Output Binding

Şimdi sıra geldi bir de Output Binding denemeye. Nesneler adındaki table'dan veri aldık. Aldığımız veriyi biraz değiştirip bu sefer Cikanlar adındaki table'a atacağız.

[function.json]

{
  "bindings": [
    {
      "queueName": "ornekkuyruk",
      "connection": "AzureWebJobsStorage",
      "name": "kuyruk",
      "type": "queueTrigger",
      "direction": "in"
    },
    {
      "name": "gelenNesne",
      "type": "table",
      "tableName": "Nesneler",
      "partitionKey": "Istanbul",
      "rowKey": "{queueTrigger}",
      "connection": "AzureWebJobsStorage",
      "direction": "in"
    },
    {
      "name": "cikanNesneler",
      "type": "table",
      "tableName": "Cikanlar",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ],
  "disabled": false
}

Bir önceki örneğin üzerine adı cikanNesneler olan, tipi table olan ve direction'ı da out olan bir binding daha ekledik. Tablo adı olarak da Cikanlar'ı verdik. Azure Functions tarafına bu tablo ile ilgili bir ICollector gelecek.

[run.csx]

public static void Run(string kuyruk, OrnekObje gelenNesne, ICollector<OrnekObje> cikanNesneler, TraceWriter log)
{
    log.Info($"C# Queue trigger'dan gelen: {kuyruk}");
    log.Info($"Table Services'dan gelen metin: {gelenNesne.Metin}");
    gelenNesne.Metin += "OK";
    OrnekObje gidenObje = new OrnekObje();
    gidenObje.PartitionKey = gelenNesne.PartitionKey;
    gidenObje.RowKey = gelenNesne.RowKey;
    gidenObje.Metin = $"{gelenNesne.Metin} alındı";
    cikanNesneler.Add(gidenObje);
}

public class OrnekObje
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Metin { get; set; }
}

Yine önceki örnek üzerinden ilerliyorum. gidenObje adında ve OrnekObje tipinde bir değişken daha tanımladım ve Metin kısmına da bir önceki nesnenin metninin üzerine bir şeyler ekleyerek yeni bir değer verdim. Unutmayın ki bu nesne Cikanlar adındaki yeni tablomuza gidecek. O nedenle ParitionKey ve RowKey değerlerini de aynı bıraktım. Bu şekilde ICollector'ı kullanarak birden çok obje gönderebilirsiniz.

Bu örneği çalıştırıp kuyruğa "Sisli" adında bir mesaj eklediğimde ilk olarak Nesneler tablosundaki nesnenin Metin değerinin sonunda "OK" ekleniyor. Sonrasında da yeni Cikanlar tablosuna yeni bir satır ekleniyor.

İkinci table'a çıktıyı aldık

Bu noktada ufak bir sorun var :) Farkındaysanız iki ayrı tabloda işlem yapıyoruz ve daha önce de bahsettiğim gibi tüm bunlar Function bittiğinde commit oluyor. Peki ya birinde hata alırsak? Çok basit :) hata aldığınız gerçekleşmez, diğeri gerçekleşir. Yani herşey en sonda commit olsa da bütün function bir transaction'dadır gibi hayallere kapılmayalım :) (Bu arada unutmadan özellikle ICollector'ın Add vs özellikleri sona kalmaz, anında çalışır) Özellikle varsayılan ayarlarda 5 defa Function'ın tekrar deneneceğini de düşünürseniz içeride tek bir operasyondan aldığınız tek bir exception yüzünden diğer bütün operasyonların ışık hızında 5 defa yapılabilir :) O nedenle Idempotent tasarım bu noktada çok kritik.

İstediğin gibi filtrele

Bu bölüme başka başlık bulamadım :) Hatırlarsanız başlarda bir yerlerde table services'dan alacağınız veriye karar vermek için kullanacağınız filtrelemenin ille binding içerisinde olmasının sınırlayıcı olabileceğinden bahsetmiştim. Alternatif olarak ParitionKey ve RowKey'i bindinge karıştırmadan filtreleme işini tamamen Azure Function içerisinde de yapabilirsiniz.

[function.json]

{
  "bindings": [
    {
      "queueName": "ornekkuyruk",
      "connection": "AzureWebJobsStorage",
      "name": "kuyruk",
      "type": "queueTrigger",
      "direction": "in"
    },
    {
      "name": "gelenNesne",
      "type": "table",
      "tableName": "Nesneler",
      "connection": "AzureWebJobsStorage",
      "direction": "in"
    },
    {
      "name": "cikanNesneler",
      "type": "table",
      "tableName": "Cikanlar",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ],
  "disabled": false
}

İlk olarak function.json dosyasında table inputbinding'den PartitionKey ve RowKey'e dair herşeyi kaldırıyoruz. Bu noktada artık sadece bir table'ın bir referansını almış olacağız. Ek bir binding yapılmayacak.

[run.csx]

 #r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Table;
public static void Run(string kuyruk, IQueryable<OrnekObje> gelenNesne, ICollector<OrnekObje> cikanNesneler, TraceWriter log)
{
   OrnekObje tekNesne = gelenNesne.Where(p => p.PartitionKey == "Istanbul" && p.RowKey == kuyruk).SingleOrDefault();

   log.Info($"C# Queue trigger'dan gelen: {kuyruk}");
   log.Info($"Table Services'dan gelen metin: {tekNesne.Metin}");
   tekNesne.Metin += "OK";

   OrnekObje gidenObje = new OrnekObje();
   gidenObje.PartitionKey = tekNesne.PartitionKey;
   gidenObje.RowKey = tekNesne.RowKey;
   gidenObje.Metin = $"{tekNesne.Metin} alındı";
   cikanNesneler.Add(gidenObje);
}

public class OrnekObje : TableEntity
{
   public string Metin { get; set; }
}

Bir sonraki adımda ise yukarıdaki gibi birkaç değişiklik yapmamız gerekiyor. Bize Table'ın kendisi geleceği için kuyruktan gelen mesaj ile Table'ı nasıl sorgulayacağımıza biz karar vereceğiz. Bunun için de LINQ kullanmak istersek tabi ki Azure Storage SDK'i kullanmamız gerek. Üst kısımda Azure Functions'da bir harici bir assembly nasıl reference alır onu görüyorsunuz #r bu işi görüyor. Sonraki değişiklik OrnekObje ile ilgili. Nesnemizi artık TableEntityden türetmemiz gerekiyor ki rahatlıkla SDK'yi kullanabilelim. IQueryAble interface'i bunu şart koşuyor.

En sonunda artık kodun içindeki LINQ sorgusunu görebilirsiniz. Ben yine ParitionKey olarak Istanbul ve RowKey olarak de kuyruktan gelen mesajı kullandım. Siz tabi ki bu sorguyu istediğiniz gibi değiştirebilir, isterseniz birden çok row da alabilirsiniz Table Services'dan. Fakat artık sorgulamayı biz yaptığımız ve Azure Functions'ın bindingini doğrudan nesne seviyesinde kullanmadığımız için nesnelerde yaptığımız değişiklikler doğrudan table'a gitmeyecek. Yani yukarıdaki örneği çalıştırırsanız tekNesnenin Metin özelliği storage'da hiç değişmeyecek, çünkü artık onu Commit eden yok. Eğer istiyorsanız bunu elle yapmanız gerekecek.

[run.csx]

 #r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Table;
public static void Run(string kuyruk, CloudTable gelenNesne, ICollector<OrnekObje> cikanNesneler, TraceWriter log)
{
   TableOperation operation = TableOperation.Retrieve<OrnekObje>("Istanbul", kuyruk);
   TableResult result = gelenNesne.Execute(operation);
   OrnekObje tekNesne = (OrnekObje)result.Result;
   tekNesne.Metin += "OK";

   operation = TableOperation.Replace(tekNesne);
   gelenNesne.Execute(operation);
}

public class OrnekObje : TableEntity
{
   public string Metin { get; set; }
}

Hem inputBinding'de hem de outputBinding'de eğer metod imzasında yukarıdaki gibi obje tipini CloudTable olarak tanımlarsanız azami esnekliğe sahip olur ve Azure SDK ile beraber gelen tüm TableOperation'ları kullanabilirsiniz. Yukarıdaki örnekte hem sorgulama işinin, hem de nesne güncelleme işinin tamamen Azure Storage SDK ile yapılmış halini görebilirsiniz.

Kolay gelsin ;)

Bu yazıyı yazmam 65 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 3 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Belirli zamanlarda veya aralıkla iş yapmak hep problem olmuştur :) Hem istediğim zamanda ve aralıkta çalışmasından emin olmak isteriz hem de altyapıyı ölçeklendirdiğimizde söz konusu işin yine bir defa çalışmış olmasını isteriz :) Hayat zor :) Şaka bir yana, tüm bunlar için Azure Functions içerisinde kullanımı süper basit bir yapı var, adı da timerTrigger. Gelin hızlıca detaylarına göz atalım.

[function.js]

{
  "bindings": [
    {
      "schedule": "0 */2 * * * *",
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in"
    }
  ],
  "disabled": false
}

İlk olarak yukarıdaki gibi Azure Functions bindingimizi ayarlıyoruz. schedule kısmında bir CRON Expression kullanıyoruz. Basit bir şekilde {saniye} {dakika} {saat} {gün} {ay} {haftanın günü} formatında bir tanımdan bahsediyoruz. Bizim yukarıdaki örneğimizde timer beş saniyede bir çalışacak. name parametresinde verdiğimiz değer birazdan yazacağımız Azure Functions'a geçecek olan parametrenin de adı olacak. Timer'ımıza function içerisinden de ulaşabileceğiz. type parametresi bindingin tipini belirliyor malum :) timerTrigger kullanarak geçiyoruz. Son olarak direction için de tabi ki in kullanmamız gerek, zaten farklı bir seçenek pek de olası değil.

[run.csx]

public static void Run(TimerInfo myTimer, TraceWriter log)
{
    if (myTimer.IsPastDue)
    {
        log.Info("Geç kalmışız!");
    }
    log.Info($"Timer {DateTime.Now} zamanında çalıştı.");
    log.Info($"5 dakika sonra çalışma zamanı {myTimer.Schedule.GetNextOccurrence(System.DateTime.Now.AddMinutes(5)).ToString()}");
    log.Info($"Son çalışan zaman {myTimer.ScheduleStatus.Last.ToString()}");
}

Yukarıda da Function kodumuzun kendisini görebilirsiniz. myTimer olarak gelen parametre Timer'ın kendisi. IsPastDue daha önce kaçırılmış bir zamanlamanın çalıştırılıp çalıştırılmadığı dönüyor. Eğer fonksyon çalışma gerektiği zamanda çalıştırılmadı ve sonradan çalıştırılıyorsa burada "True" dönecektir. Tabi buradan çalıştırma isteklerinin kuyruklandığı fikrine falan kapılmayın :) Buradaki durum daha fazla Azure Function'ın çalışması için gerekli ortamın karşılaşabileceği sıkıntılarla alakalı. Örneğin birazdan bahsedeceğimiz blob lease için Storage Account'a ulaşılamamış olabilir. Bu gibi durumlarda Function Timeout'a uğramadan önce gecikirse IsPastDue true gelecektir.

Daha sonraki adımlarda TimerInfo üzerinden alabileceğimiz bazı ek bilgileri de göstermek istedim. Örneğin GetNextOccurrence ile bir zaman verip o zamandan sonra ne zaman Timer'ın çalışacağını alabiliyorsunuz. Bizim timer iki dakikada bir çalışacak ve ben beş dakika sonra hangi zamanda tekrar çalışacağını soruyorum TimerInfo'ya. Son satırda da ScheduleStatus.Last diyerek en son çalıştığı zamanı alıyorum. Eğer bir dakikadan kısa süreli timerlar kullanıyorsanız ScheduleStatus null gelecektir. Bu da hardcoded bir kural.

iki dakikada bir Timer çalışıyor.

Yukarıda da gördüğünüz gibi Timer ile ilgili bir sıkıntımız yok. Normal şartlarda Azure Functions'da Consumption Plan'daysanız birazdan soracağım soruyu sormanız anlamlı olmaz fakat özellikle App Service Plan'daysanız App Service Plan'ı scale ettiğinizde Timer'larınızın her sunucuda çalışıp çalışmayacağından, yani daha basit bir tabirle tek instance olup olmadıklarından emin olamayabilirsiniz. Cevabı; hiç fark etmez. timerTrigger'lar her zaman Singleton ;) Bunu başarabilmek için de host.json'daki storage account ayarlarından gidip bir blob lease alıyor runtime. Eğer function çalıştığında lease alamazsa zaten çalıştığını varsayacak. Eğer bir hata nedeniyle lease bırakılmamışsa bu sefer de tabi ki timeoutu beklemeniz gerekecek.

[host.json]

{
  "singleton": {
    "listenerLockPeriod": "00:01:00"
  }
}

Varsayılan ayarlarda blob lease'ler bir dakikalığına alınır. Fakat isterseniz bunu host.json içerisinde listenerLockPeriod değeri vererek değiştirebilirsiniz. Lock'ı kısaltmak hata durumlarında toparlama hızını da arttıracaktır çünkü sonuç itibari ile timeout beklemek zorunda kalacak bir sonraki instance ama tabi bir de lease yenileme maliyeti var. Eğer Azure Function'ınız 60 saniyeden uzunsa lease'in yenilenmesi gerekir, yoksa iki instance sahibi olabilirsiniz. Bunun için varsayılan ayar da listenerLockPeriod süresinin yarısında lease'in yenilenmesi şeklinde. Özetle, 30 saniyelin bir lock 15 saniyede bir lease yenileme anlamına gelir ki bu da toplam Storage Account transaction sayınızı ikiye katlar. Söylemedi demeyin :)

Bundan sonraki artık size kalmış. İsterseniz timerTrigger ile beraber Queue Binding kullanarak belirli sürelerde kuyruğa iş atarsanız, isterseniz Blob Binding maceralara atılırsınız :) tercih size kalmış. Tabi bunların hiçbirini kullanmayıp istediğiniz kodu da Azure Functions'da çalıştırabilirsin.

Görüşmek üzere.

Bu yazıyı yazmam 88 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 6 dakikada, göz gezdirerek ortalama 2 dakikada okuyabilirsiniz ;)

Azure Functions'da kullanabileceğimiz Trigger yapılarından biri de QueueTrigger. Azure Storage hizmetinin bir parçası olan Queue Storage genelde Web ve Worker Role'lerin birbirinden bağımsız olarak ölçeklendirildiğinde birbirleri ile konuşabilmesi için kullanılıyor. Bu yazıdaki amacım tabi ki Queue Storage'ı anlatmak değil. O konuyu merak edenler için daha tavsiyem daha önce yazdığım bu yazıyı okumaları ;)

Queue Trigger Tanımlamak

Queue Trigger tanımlama işimize yeni yaratacağımız bir Function'ın function.js dosyasından başlayacağız.

[function.js]

{
    "disabled": false,
    "bindings": [
      {
        "name": "queueJob",
        "queueName": "samplequeue",
        "connection": "AzureWebJobsStorage",
        "type": "queueTrigger",
        "direction": "in"
      }
    ]
}

Parametrelere bakacak olursak, name bizim functiona Queue mesajını taşıyacak olan parametrenin adı olacak, queueName dinleyeceğimiz kuyruğun adı olacak (kullanacağımız storage hesabında bu isimde bir kuyruk yaratmamız gerek), connection kısmında appsettings.json'a koyacağımız storage connection stringini key adını yazıyoruz ki functions hangi storage account'a gideceğini bilsin ve son olarak type için doğal olarak queueTrigger diyerek direction olarak da in diyoruz. Böylece queueTrigger tipinde bir inputTrigger yaratmış olduk ve hangi storage account'taki hangi kuyruğu dinlemek istediğimizi ve hangi parametre ismi ile bizim function'a gönderilmesi gerektiğini de belirtmiş olduk.

[appsettings.json]

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AzureWebJobsDashboard": "UseDevelopmentStorage=true"
  }
}

AppSettings.json dosyamız yukarıdaki şekilde local storage emülatörünü gösteriyor. Buradaki AzureWebJobsStorage connection stringini zaten function'ı tanımlarken de kullanmıştık. samplequeue adındaki kuyruğu Azure Functions Runtime bu storage account içerisinde arayacak. Localde test edebilmek için Azure Storage Explorer kullanarak local emülatörü bağlanıp elle gerekli kuyruğu yaratabilirsiniz.

[run.csx]

using System;

public static void Run(string queueJob,
    DateTimeOffset expirationTime,
    DateTimeOffset insertionTime,
    DateTimeOffset nextVisibleTime,
    string queueTrigger,
    string id,
    string popReceipt,
    int dequeueCount,
    TraceWriter log)
{
    log.Info($"C# Queue trigger function çalıştı: {queueJob}\n" +
        $"queueTrigger={queueTrigger}\n" +
        $"expirationTime={expirationTime}\n" +
        $"insertionTime={insertionTime}\n" +
        $"nextVisibleTime={nextVisibleTime}\n" +
        $"id={id}\n" +
        $"popReceipt={popReceipt}\n" +
        $"dequeueCount={dequeueCount}");
}

Yukarıda en basit hali ile bir log atan Azure Function var. Gelin şimdi bu function'ın parametrelerini inceleyelim.

  • queueJob zaten bizim function.js içerisinde name olarak verdiğimiz parametre. Bu parametreyi biz şimdilik string olarak tanımladık. Azure Functions Runtime'ı kuyruktan gelen mesaj string olarak deserialize edip bize verecek. Ben bu örnekte kuyruktaki mesaja "ornekmesaj" yazmıştım ve bu string parametrede de geriye o metin geldi.
  • queueTrigger parametresi ilk bakışta kafa karıştırabilir çünkü bu parametrenin yaptığı iş aynı bizim queueJob gibi string olarak kuyruktaki mesajın içeriği dönmek. Peki neden böyle iki parametre var? Çünkü eğer isterseniz queueJob parametresini string olarak değil de CloudQueueMessage tipinde de alabiliyorsunuz. Bu durumda ayrıca bir de string olarak aliyim derseniz queueTrigger'ı kullanabilirsiniz.
  • expirationTime kuyruktaki mesajı işlemezseniz ne kadar sürede expire edeceğini verir.
  • insertionTime kuyruktaki mesajın kuyruğa eklediği zamanı verir.
  • nextVisibleTime kuyruktan alınan bir mesajın alındıktan sonra başarılı bir şekilde işlenmezse ne zaman tekrar kuyrukta görünür olacağını verir. Eğer input parametre tipini CloudQueueMessage olarak alırsanız bu süreyi uzatma şansınız olabilir.
  • Id bu queueItem'ın unique ID'sini verir.
  • popReceipt ise eğer CloudQueueMessage mesaj ile manual işlemler yapmak isterseniz işinize yarayabilir. O anki Azure Functions instance'ının kuyruktan mesajı alırken elde ettiği popReceipt'ı size verir. Böylece eğer elinizdeki işlem uzun sürer ve başka bir instance da kuyruktan aynı işi alırsa popReceipt ile storage API'larına gittiğinizde geriye hata alma şansınız olur. Genelde popReceipt kullanımını Azure Stroge SDK zaten kendi içerisinde hallediyor. Ama Azure Storage'ın REST API'larına kendiniz gitmek isterseniz bir queue mesajına bir delete veya update yapmanız için elinizdeki kesinlikle popReceipt'ınızın olması gerekir. Bir anlamda sizin kuyruktan mesajı alma biletiniz diyebiliriz. İtiraf etmek gerekirse bir Azure Functions içerisinde bu parametreyi kullanma ihtimaliniz epey düşük.
  • dequeueCount parametresi ile bir mesajın kuyruktan kaç defa alındığını verir. Sürekli kuyruktan aldığınız fakat bir türlü başarılı bir şekilde işleyemediğiniz mesajları işlemeyi kaç defa deneyeceğinizi belirlemeniz gerekiyor. Zehirli mesajlar olarak da adlandırılan bu mesajlar bir süre sonra sayı olarak artarsa sürekli kısır döngüde aynı mesajları işlemeye çalışıp hep başarısız olan bir ortama sebep verebilir. Buna engel olmak için dequeueCount kullanarak bir mesajın kaç defa denendiğini görebilirsiniz. Varsayılan ayarlarda Azure Functions bir mesajı 5 defa işlemeyi deneyip sonra doğrudan samplequeue-poison kuyruğuna taşıyacaktır. Bu kuyruğun adı tahmin edeceğiniz üzere sürekli olarak sizin orijinal kuyruk adınızın sonuna -poison eklenerek oluşturulur. Poison (zehirli) mesaj kuyruğunu dinleyerek gerekli işlemleri yapmak da artık size kalıyor.
Queue Trigger Log çıktısı

Bir mesajın başarılı bir şekilde işlenip işlenmediğine Azure Functions nasıl karar verir? diye sorarsanız, cevabı basit. Eğer function geriye bir exception dönmüyorsa alınan mesaj başarılı bir şekilde işlendi demektir.

Runtime Konfigürasyonu

Azure Functions'daki qeueTriggerlar ile ilgili yapabileceğimiz bazı özelleştirmeler var. Bunları host.json dosyası içerisinde yapabiliyoruz ve bir Function App genelinde geçerli oluyor.

[host.json]

{
  "queues": {
    "maxPollingInterval": 2000,
    "batchSize": 16,
    "maxDequeueCount": 2,
    "newBatchThreshold": 8
  }
}

  • maxPollingInterval Azure Functions Runtime'ının ne kadar sürede bir kuyruğu kontrol edeceğini belirler. Varsayılan ayarlarda iki saniyede bir kuyruğa gidip yeni bir job olup olmadığı kontrol edilir. Bunu isterseniz burada değiştirebilirsiniz.
  • batchSize Aynı anda paralel olarak alınabilecek görev sayısını belirler. Varsayılan ayar 16, azami verebileceğiniz değer ise 32. Bu değer size az gelirse maalesef daha fazla arttırmak için birden fazla Function veya Thread ile uğraşmak zorundasınız. Bu konuda UserVoice üzerinde bir istek var, oy vermek isterseniz buyurun :)
  • maxDequeueCount dan daha önce bahsetmiştik. Varsayılan ayar beş defa bir mesajı denemek şeklinde. Bu sayıyı istediğiniz gibi değiştirebilirsiniz.
  • newBatchThreshold yeni bir Batch alınması için ulaşılması gereken asgari batch batch sayısını tanımlar. Varsayılan ayarlarda bu batchSize'ın yarısı olarak tanımlanır. Örneğin batchSize için aynı anda 16 mesaj alınabileceğini ayarladıysanız newBatchThreshold 8 olacaktır ve eldeki paralel olarak işlenen mesaj sayısı 8'e düşmedikçe yeni bir 16'lık Patch alınmayacaktır. newBatchThreshold değerini değiştirerek kuyruktan aynı anda daha fazla mesaj alınmasını sağlayabilirsiniz fakat özellikle Consumption Plan yerine klasik App Service Plan kullanıyorsanız RAM/CPU tüketimi adına aşırı paralelleşmekten uzak durmak da isteyebilirsiniz. Dikkatli olmakta fayda var :)

Queue Binding

Bu noktaya kadar bir queue trigger tanımlayıp kuyruğa mesaj atıldığında onu işlemeyi gördük. Gelin bir de bir kuyruktan diğerine mesaj atma konusuna bakalım. Özetle, queue bindingleri kullanarak birden çok kuyruk arasında iletişim sağlayacağız.

[function.js]

{
  "disabled": false,
  "bindings": [
    {
      "name": "queueJob",
      "queueName": "samplequeue",
      "connection": "AzureWebJobsStorage",
      "type": "queueTrigger",
      "direction": "in"
    },
    {
      "name": "queueJobOutput",
      "queueName": "samplequeueout",
      "connection": "AzureWebJobsStorage",
      "type": "queue",
      "direction": "out"
    }
  ]
}

Yukarıdaki örnekde ikinci bir binding daha görüyorsunuz. Yine name parametresindeki değer bizim function'ın imzasında yer alacak. queueName bu sefer yeni bir output kuyruğunun adı. connectionımız aynı, böylece aynı storage hesabını kullanmış olacağız. Son olarak binding tipimiz queue ve direction da out şeklinde ayarlanmış durumda.

[run.csx]

using System;

public static void Run(string queueJob, out string queueJobOutput, TraceWriter log)
{
    queueJobOutput = queueJob + " devam....";
}

Bu sefer function kodunu biraz daha temiz tutmak istedim. Basit bir şekilde input ve output parametrelerimiz var. queueJobOutput parametresini zaten output bindingimizi tanımlarken kullandığımız name değeri oldu. Bu functionın yapacağı şey bir kuyruğa mesaj eklendiğinde tetiklenip gelen mesajın sonunda " devam..." metnini ekleyip yeni kuyruğa yeni bir mesaj olarak eklemek olacak. Eminim siz daha anlamlı senaryolar düşünebilirsiniz :)

[run.csx]

using System;

public static void Run(string queueJob, ICollector<string> queueJobOutput, TraceWriter log)
{
    queueJobOutput.Add(queueJob + " devam...)");
    queueJobOutput.Add(queueJob + " daha da devam...");
}

Eğer aynı kuyruğa birden çok mesaj / görev atmanız gerekirse bu sefer ICollector'ı kullanabilirsiniz. Yukarıdaki örnekte bizim kaynak kuyruktan gelen görevi alıp iki farklı görev (queue job) yaratıp output bindingde tanımlı kuyruğa gönderiyoruz.

POCO Kullanımı

İsterseniz bindinglerde kendi özel objelerinizi de kullanabilirsiniz.

[run.csx]

using System;

public static void Run(string queueJob, out OrnekMesaj queueJobOutput, TraceWriter log)
{
    queueJobOutput = new OrnekMesaj() { Metin = $"{queueJob} devam..." };
}

public class OrnekMesaj
{
    public string Metin { get; set; }
}

Yukarıdaki örnekte kaynak kuyruktan gelen metnin üzerine " devam..." metnini eklerken artık geriye basit bir String olarak değil de custom OrnekMesaj nesnesi ile gönderiyoruz. Buradan yeni kuyruk objesine, göreve deserialize işlemini JSON deserializer kullaran Azure Functions Runtime kendisi halledecek. Aynı işlemi input binding'lerde de kullanabilirsiniz.

Output Binding'de JSON Deserialization

Yukarıdaki ekran görüntüsünde yaptığımız örneklerin output binding sonuçlarını görebilirsiniz.

Kolay gelsin ;)

Bu yazıyı yazmam 22 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 2 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Son zamanlarda Azure Functions yazıları yazmaya başlayınca özellikle WebJobs ile arasındaki benzerlikleri dikkat çekmiyor değil. Durum böyle olunca detaylı bir karşılaştırma yapalım istedim.

Comsumption Plan : App Service Plan

Azure Functions'ın en büyük özelliği "Serverless" olması. Bir anlamda Microsoft'un FaaS (Function-As-A-Service) hizmeti olan Azure Functions'ta fiyatlandırma ve kullanım ölçümü GB-s üzerinden yapılıyor. Yani burada saniye başına kullanılan memory ve işlemci'den bahsediyoruz. Maksimum 1.5GB memory'ye kadar yükselebiliyor bir function'ın bellek alanı. WebJobs'a baktığımızda ise ölçeklendirilmesinin tamamen App Service Plan'a dayalı olduğunu görüyoruz. Bu durumda ufak bir WebJob için bile (Hele bir de Continious Run derseniz "Always On" da gerekeceği için Basic Tier şart olacak) bir VM almak zorunda kalabiliyorsunuz. WebJob'ınızı ne kadar çalıştığını vs kimse umursamıyor ve kullansanız da kullanmasanız da VM'in parasını ödemek zorunda kalıyorsunuz.

Bazılarınız zaten hali hazırda App Service Plan'larımız var diyebilirler. O durumda hem Azure Functions'ı hem de WebJob'larınızı isterseniz klasik App Service Plan'lara da atabilirsiniz. Fakat tabi ki bu durumda Azure Functions'ın Serverless özelliğinden kopmuş oluyorsunuz.

HTTP API Sunarken

Webjobs ile HTTP API host etmek ayrı bir dertti. WebJobs kendisi Kudu SCM üzerine kurulu olduğu için HTTP endpoint verebilse de Kudu'nun authentication yapısından geçmek zorunda kalıyorduk. Bu da tabi ki API açmak isteyen bir developer için pek de anlamlı değil. Azure Functions'a baktığımızda ise çok daha geniş authentication seçenekleri görüyoruz. Azure Active Directory, Facebook, Google, Twitter, LiveID vs diyerek liste uzuyor. Ayrıca Azure Functions API Metadata'sını da OpenAPI specificationu ile sunabiliyor.

Visual Studio ve Araçlar

Visual Studio tarafında Webjobs biraz daha avantajlı. 2014 yılından beridir ortalıkla olduğu için WebJobs'ın Visual Studio entegrasyonu çok daha iyi. Azure Functions'ın ise Visual Studio araçları çıkalı daha bir hafta olmadı ve şu an için Preview'da. Fakat, Azure Portal'ına giderseniz tam tersi bir durum göreceksiniz. WebJobs için bir kod yazma ortamı yokken Azure Functions için full bir kod yazma ortamı söz konusu. Rahatlıkla Azure Portal'ına gidip bir function'ı sıfırdan yazıp, çalıştırabilir ve hatta debug bile edebilirsiniz.

Geri kalanlar

Bunların dışında her iki platform arasında bir fark yok. Trigger'lar aynı, binding özellikleri aynı. Birinin diğerinden üstün bir özelliği en azından ben bu yazıyı yazarken yok.

Sorularınız olursa aşağıda yorum olarak alabilirim ;)

Görüşmek üzere.

Bu yazıyı yazmam 112 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 5 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Azure Functions aslında bakarsanız eski WebJobs'ın üzerinden ilerlenilerek geniştirilmiş bir hizmet. O nedenle ikisi arasında süper benzerlikler var. Birazdan anlatacaklarım WebJobs deneyimi olanlar için epey tanıdık gelecektir. Nitekim 2014'te tam da bu konuda WebJobs yazısı yazmıştım :)

Gelelim konumuza, olayımız Azure Functions için farklı triggerlar kullanmak. Bunlardan httpTrigger'ı geçen bir yazıda görmüştük. Bu yazıda ise blobTrigger'a bakarak Azure Functions için Input Binding konusunu da göreceğiz.

Blob Trigger Ayarlamak

İlk olarak yeni bir Azure Function ekliyoruz. İçindeki functions.json'ı değiştirerek bir blobTrigger tanımı yapacağız. Aşağıdaki şekilde solution'ı şekillendirmek için ister Visual Studio için yeni gelen araçları kullanın, ister elle dosyaları oluşturun, sonuç fark etmeyecek.

Blob Trigger Örnek Proje Yapısı

[function.json]

{
  "disabled": false,
  "bindings": [
    {
      "name": "myBlob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "blobcontainer",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

Gelin yukarıdaki parametreleri tek tek inceleyelim. Bindings collection'ı içerisinde şu an için bir tane binding var. Bu binding'in adının ne olduğu şu an için önemli değil, fakat bu adı "myBlob" olduğunu unutmayın. Bir sonraki adımda bunun nereye denk geleceğini göreceğiz. Binding'in type bilgisi blobTrigger olarak tanımlanmış. Bunu da anlamak pek zor değil :) Runtime'a bir blobTrigger kullanacağımızı burada belirtmiş oluyoruz. Sonraki direction parametresi önemli. Şu an için bir InputBinding yaratıyoruz. Böylece dışarıdan içeriyi tetikleme işlemi yapacağımızı da belirtmiş oluyoruz. OutputBinding olayı da söz konusu, ona da birazdan bakacağız. Gelelim son iki parametreye; birincisi path. Path parametresi dinleyeceğimiz Storage hesabının neresini dinleyeceğimizi belirliyor. Ben buradan blobContainer yazdım. Storage Account içerisinde de blobcontainer diye bir container olduğunu varsaydım. Örneği çalıştırırken bu containerı elle yaratmak gerekecek. İkinci parametre olan connection ise Storage Account'a ulaşmak için gerekli olan connection stringi alacağımız appsettings.json da key/value çiftinin key name'i.

[appsettings.json]

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AzureWebJobsDashboard": "UseDevelopmentStorage=true"
  }
}

Ben örnekte local emülatörü kullandığım için appsettings.json'daki tüm değerleri de bu şekilde ayarladım.

Input Binding

[run.csx]

using System;

public static void Run(string myBlob, TraceWriter log)
{
    log.Info($"Blobun içinde ne var?: {myBlob}");
}

Sıra geldi Azure Function'ın kendisini yazmaya. Yukarıdaki gibi functionı yazarken dikkat etmemiz gereken tek nokta ilk parametre olan string parametrenin adının blobTrigger'ı tanımlarken kullandığımız name ile aynı olması. Buradaki bu parametreye doğrudan blob'un içeriği gelecek. Ben string kullandım çünkü containera bir TXT dosyası atacağım ama siz isterseniz bu parametreyi TextReader, Stream, ICloudBlob, CloudBlockBlob veya CloudPageBlob tipinde de tanımlayabilirsiniz. Functions runtime'ı deserialization işlemini halledecektir. Aslına bakarsanız eğer blob içerisinde schemasından emin olduğunuz bir JSON varsa doğrudan o tipi de verip geçebilirsiniz. Azure Functions bunu otomatik olarak deserialize edecektir. (Not: Şu an bu konuda bir bug var ve github'da da bu konuda Issue var :))

Blob Trigger çalışıyor

Yukarıdaki ekran görüntüsünden de anlayabileceğiniz üzere yeni bir blob geldiği anda içindeki metne ulaşabiliyoruz. Benim örneğimdeki metin "hop hop" şeklindeydi :)

Detaylar

Bu noktada özellikle blobTriggerlar ile ilgili bahsetmemiz gereken birkaç şey var. Azure Functions blobları takip edebilmek ve yaratılan bir blob için sadece bir defa çalışabilmek adına bazı kayıtlar tutuyor. Bu kayıtları tutmak için de yine storage account içerisinde azure-webjobs-hosts adında bir container yaratıyor. İsterseniz bu containerın farklı bir storage account'da olmasını da sağlayabilirsiniz. Runtime'ın baktığı config değeri appsettings.json'daki AzureWebJobsStorage altında saklı olmalı. Ben yaptığım örnekte bu değeri blobtrigger yaratırken de connection string olarak kullandım, ama siz bunları ayırabilirsiniz.

Diğer bir nokta ise bir türlü işlenemeyen bloblar. Bizim örnekte tabi bir olay yok, sadece okuma ve loglama yaptık ama sizin fonksiyonunuz tabi ki yeri geldiğinde hata da alabilecek işlemler yapıyor olabilir. Vazsayılan ayarlarında Azure Functions bir blobu işlemeyi en fazla 5 defa dener. Eğer beş defada bir blob başarılı bir şekilde fonksiyonunuz tarafından işlenemezse AzureWebJobsStorage ayarında belirlediğiniz storage account içerisinde webjobs-blobtrigger-poison adındaki kuyruğa bir mesaj atılır. Bu mesaj içerisinde FunctionId, blob tipi, container adı, blob adı ve etag bulunur. Bundan sonrasında konuyu çözmek size kalıyor.

BlobTrigger ile ilgili hoşunuza gitmeyecek olan bir diğer haber ise özellikle blob sayısı çok arttığında bir yavaşlama olması. Biraz önce de bahsettiğimiz gibi aslında yeni gelen blobları anlamanın yöntemi alınan kayıtlar ile blobları karşılaştırmak. Bu durumda özellikle binlerce blob söz konusu olduğunda ciddi bir yavaşlığa neden olabiliyor. Eğer anında tepki verebilmek istiyorsanız ileriki yazılarda bahsedeceğim QueueTrigger'ı kullanmak daha doğru olacaktır. Yok illa BlobTrigger kullanacağım diyorsanız bir tavsiyem trigger source olarak ayarladığınız containerı temiz tutarak tetikleyen blobları silmeniz. Unutmadan, azure-webjobs-hosts altındaki kayıtları da silinmiş bloblar için silmenizde fayda var.

Output Binding

Yazıyı bitirmeden output binding'e de bir bakalım. İlk önce function.json da bir output binding tanımlamamız gerekecek.

[function.json]

{
  "disabled": false,
  "bindings": [
    {
      "name": "myBlob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "blobcontainer/{blobname}",
      "connection": "AzureWebJobsStorage"
    },
    {
      "name": "yourBlob",
      "type": "blob",
      "direction": "out",
      "path": "blobcontainer2/{blobname}",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

Tanımladığımız ikinci Binding'in adı yourBlob :) Bu aynı anda bizim Azure Function'ın da output parametresi olacak. Binding tipimiz blob ve direction out. Buraya kadar herşey eskisiyle aynı. path kısmında olay biraz ilginçleşiyor. Bizim örnekte yapacağımız şey elimize gelen blobu aynı alıp başka bir container'a kopyalamak. Bu durumda tabi ki bir de blobun adını bilmek gerek. Aksi halde kopyalayacağımız yerde bloba ne isim vereceğimizi bilemeyiz. Bunun için path bilgisinin içine {blobname} değişkenini koydum. Aynı değişkeni çaktırmadan input bindingin içindeki pathe de ekledim. Böylece input binding ile gelirken blobun adı gelecek ve doğrudan output bindingdeki yerini alacak. Böylece binding ayarlarımızı tamamlamış olduk.

[run.csx]

using System;

public static void Run(string myBlob, TraceWriter log, out string yourBlob)
{
    yourBlob = myBlob;
}

Yukarıdaki kod ise final Azure Functions kodumuz. Kodun imzasında bakarsanız yeni bir output parametre göreceksiniz. İşte bu output parametre ile yeni blob içeriğini yeni container'a göndereceğiz. Function'ın içinde pek bir şey yaptığımız yok :) Siz daha anlamlı şeyler bulursunuz diye tahmin ediyorum.

İşte hepsi bu kadar, böylece bir input trigger olarak blob alıp sonra da output binding ile blob çıkarmış olduk. Bunu da bir Azure Function olarak deploy edebiliyoruz :) Eski WebJobs bilenler için tekrar uyarımı geçiyim, Azure Functions zaten WebJobs üzerinde çalışıyor, ana farklılıklardan biri "Consumption Plan", yani deployment modeli. Yakında her iki altyapıyı karşılaştıran bir yazı yazmayı planlıyorum ;)

Görüşürüz.

Bu yazıyı yazmam 68 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 2 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Dünkü Azure Functions giriş yazısından sonra Azure Functions ortamında Debugging için ne gibi özellikler bulunduğundan bahsetmek istiyorum. Hatırlarsanız local debugging özelliğinden zaten bahsetmiştik. Hatta doğrudan projemiz açıkken F5'e basıp local Azure Functions CLI'ı indirerek projeyi localde çalıştırabilmiştik. Fakat yine hatırlarsanız :) her trigger tipinin bu şekilde çalışmayacağından da bahsetmiştim. O neden deployment yapıp test ediyor olmak bazı senaryolarda tek seçenek oluyor. Tabi, bunun haricinde, genel bir strateji olarak da ne olursa olsun son bir testi cloud ortamında, en azından staging gibi bir ortamda yapmanızda fayda var.

Remote Debugging

Azure Functions ile beraber remote debugging yapma şansımız var. Yani localdeki Visual Studio'yu alıp cloud ortamındaki Functions App'e bağlayabiliyoruz.

Remote Debugging

Bunun için tabi ki ilk başta bir deployment yapmanız şart :) Sonrasında Visual Studio içerisinde "Server Explorer"a giderek Azure Subscriptionı'nızı bağlamanız gerekiyor. Son olarak da App Service'i buldunuz mu sağ tıklayın ve "Attach Debugger" deyin. Sonrasında web portalına gidip bir test request'i gönderebilirsiniz. Bu arada, eğer web portalında function'ınız görünmez olursa bu bir bug :) tekrar bir deployment yaparsanız herşey sakinleşecektir.

Remote Debugging

Yukarıdaki ekran görüntüsünde de görüldüğü üzere req.RequestUri'a baktığımızda doğrudan production / live ortamında Uri gözüküyor. Böylece hızlı bir şekilde live ortama debugger ataçlamak mümkün. Ben ne kadar "hızlı" desem de aslında debugger epey yavaş çalışıyor, ama o kadar olur değil mi? :)

Logging

Debugging konusunda bir diğer yardımcınız da tabi ki loglarınız. Eğer web portalı üzerinden Azure Functions'ı çalıştırdıysanız aşağıdaki pencere dikkatinizi çekmiştir.

Azure Functions Logları Portalda

Bu pencerede hem functions içerisinde attığımız trace bilgilerini hem de bazı ekstra durumlara ait bilgileri görebiliyoruz. Aslında bunların hepsi biz bir Azure Function App Service yaratırken beraberinde otomatik olarak gelen Storage Account'a atılıyor.

Azure Functions için oluşturulan Storage Account

Eğer bu Storage Account'a ayrıca Azure Storage Explorer ile bağlanırsanız normalde portalda gözükenden çok daha fazla data görebilirsiniz. Unutmadan, bu Storage Account'un maliyeti Azure Functions'ın fiyatlandırmasına dahil değil :) Benden söylemesi.

Storage Account'a bağlandığınızda Table Services kısmına göz atarsanız her ay için ayrı bir table yaratıldığını görebilirsiniz. Bu table'lar içerisinde de bizim koddan attığımız loglar bulunuyor. Portalde gördüğünüz logların ciddi bir kısmını portal kendisi atıyor, logluyor. Bunu özellikle debugging esnasında işe yarasın diye yapmışlar. Fakat productiondan giden triggerlar için daha temiz bir şekilde sadece trigger logları tutuluyor. Yani, özetle, siz eğer bir şey loglamazsanız herhangi bir log tutulmaz. Fakat portalden test için çalıştırırsanız bol bol log bulursunuz.

Azure Functions Detaylı Logları

Yukarıdaki log verisi aşağıdaki kodun sonucu. Log atarken verdiğimiz verilen LogOutput kolonuna yerleştirilmiş. Geri kalanlar runtime'ın kendi attığı bilgiler. StartTime ve EndTime arasındaki süreye bakarsan CPU Time olarak 31 milisaniye kullanmışız.

log.Info($"C# HTTP trigger function processed a request. RequestUri={req.RequestUri}");

Aynı veriyi Azure Portal'ında da görme şansınız var.

Azure Functions Detaylı Logları

Bu yazıyı yazmam 100 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 5 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Azure Functions konusunda uzun süredir yazı yazmak istiyordum. Geri durmamın nedeni ise Visual Studio içerisindeki Tooling'inin rezalet olması, veya başka bir deyişle aslında Tooling'inin olmamasıydı :) Azure Functions kullanımı için bugüne kadar tavsiye edilen kullanım yöntemi tamamen Azure Portalı'ndaki araçlarla kısıtlıydı. Bugün Azure Functions için Tooling'in Preview sürümü duyurulunca ben de hemen hızlıca bir yazı yazmak istedim.

Azure Functions neden bu kadar seni heyecanlandırıyor?

Size bu soruyu sorduracak kadar ipucu verememiş olabilirim :) ama Azure Functions beni çok heyecanlandırıyor. Azure Functions Microsoft'un Serverless ürünü. Bilenler için AWS Lambda diyebilirim. Buradaki amaç yazılım geliştiriciyi sunucunun varlığından olabildiğince uzaklaştırmak. O kadar ki Azure Functions'ın "Consumption Plan" denilen fiyatlandırma planına bakarsanız fiyatlandırmanın GB-s (Gigabyte Seconds) gibi bir birim üzerinden yapıldığını görebilirsiniz. Peki ne demek bu? Bir function çalışırken kullandığı RAM miktarı ile çalışma süresi (saniye cinsinden) hesaplanarak birbiri ile çarpıldığında ortaya çıkan biri GB-s oluyor :) Azure Functions fiyatlandırması da tamamen bu birim üzerinden hesaplanıyor. Ayrıca her bir execution için de alınan standard bir ücret var. Her ay ilk 1 milyon execution ve 400,000 GB-s ücretsiz.

Sanırım şimdi anlamışsınızdır neden Azure Functions'ın heyecan verici olduğunu :)

Kolları sıvamaca...

Şimdi gelin "Hello World" tadında bir tur yapalım. İlk olarak Azure Functions için Preview seviyesinde olan Tooling'i buradan bir indirin. Yanlış anlaşılma olsun istemem, Azure Functions kendisi geçen Connect etkinliğinde GA oldu. Preview olan tek şey şu anda Visual Studui içerisindeki entegrasyonu.

İlk Azure Functions projemiz

Yüklemeyi bitirdikten sonra hemen yeni projeler listesinde Azure Functions'ı göreceksiniz. Yeni bir proje yaratın ve içerisinde varsayılan ayarlarlda neler geliyor bir bakalım.

Projeye Function eklerken...

Proje içerisinde gelen iki dosya var. Bunlardan ilki appsettings.json. Adından da anlaşılacağı üzere burası bizim tüm connection string vs gibi şeyleri saklayacağımız yer olacak. İkinci dosya ise host.json. Bu dosya tüm uygulama genelinde geçerli olacak, bir anlamda sistem ayarlarının bulunduğu yer.

[host.json]

{
  "http": {
    "routePrefix": ""
  }
}

Biz birazdan bir HTTP API oluşturacağız. Varsayılan ayarlarda Azure Functions tüm URL Pathlerin önüne "api" routingini ekliyor. Yukarıda gördüğünüz şekilde host.json içerisinde bunu değiştirebiliyoruz. Ben basit bir şekilde bu parametreyi boş geçerek routingi kaldırıyorum.

Hazır Function şablonları

Bir sonraki adımda artık "New Azure Function" diyerek fonksyon eklemek istediğinizde karşınıza bir şablon listesi gelecek. Buradan hazır function şablonlarından istediğinizi seçebilirsiniz. Ben bu yazıda bir API örneği yapmak istediğim için basit bir şekilde "HttpTrigger - C#" örneğini seçeceğim.

HttpTrigger yaratırken yan tarafta "Authorization level" diye bir seçenek göreceksiniz. Burada üç farklı tercihte bulunabiliyoruz. Birincisi "Function"; eğer bunu tercih ederseniz function'ın kendi anahtarı (key) ile function'a ulaşmanız gerekiyor. Eğer "Admin" seçeneğini kullanırsanız Function App'in Master Key'i ile bu functiona ulaşabilirsiniz. Tüm bu key'leri Azure Portal'ında deployment sonrası bulabilirsiniz. Eğer "Anonymous" seçerseniz doğrudan herkes ulaşabilir anlamına geliyor. Azure Portal'ında Federated Identity gibi özellikler de var, ama şimdilik makaleyi bir kitaba çevirmemek için bu konuları atlıyorum. İleriki yazılarda detaylarına bakarız.

İlk function hazır.

İlk Function'ı eklediğimiz gibi Solution Explorer'da function'ın adında bir klasör göreceksiniz. Function ile ilgili herşey buraya geliyor. İçerisindeki function.json dosyası bir functionın girdi ve çıktılarını tanımlıyor.

[function.json]

{
  "disabled": false,
  "bindings": [
    {
      "authLevel": "function",
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "name": "res",
      "type": "http",
      "direction": "out"
    }
  ]
}

Bizim örneğimizde req adında ve httpTrigger tipinde bir girdimiz var. Authorization'u da function seviyeside yapacağız. Ayrıca res" adında bir de **http outputumuz var. Bu tanımlardan kullandığımız isimler doğrudan birazdan göreceğimiz C# function'ın method imzasında yer alacak. Son olarak, project.json dosyası da her zamanki, tanıdığımız, dependency'lerimizi vs tanımladığımız yer.

[run.csx]

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
string name = req.GetQueryNameValuePairs() .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0) .Value;
return name == null ? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string.") : req.CreateResponse(HttpStatusCode.OK, "Hello " + name); }

Yukarıda run.csx dosyasının içeriği görebilirsiniz. Ben basitleştirmek adına gelen örnekten bazı noktaları kaldırdım. Şimdilik QueryString'de name adında bir parametre gelmişse değerini alıp geri döndürüyoruz. Eğer parametre gelmemişse uygun uyarı ile beraber geriye BadRequest'i paslıyoruz.

Projeyi "Start" deyip Debug modda başlattığınızda Azure Functions CLI'ı indirmek isteyip istemediğiniz sorulacak. Azure Functions için yazılmış functionlarınızın hepsini olmasa da özellikle API'ları local'de de çalıştırabilirsiniz. Kullandığınız binding tiplerine göre localde çalıştırma kısıtları mevcut. Biz HttpTrigger kullandığımız için rahatlıkla devam edebiliriz. Azure Function CLI indikten sonra function localde çalışacak ve doğrudan bir endpoint sahibi olacağız.

Local emülatör devrede

Azure Function default olarak 7071 portundan başlıyor. Sonrasında her çalıştırdığınız functionsHost 7071'den başlayarak, bir artarak devam eder.

Local emülatör devrede

Local'deki testi gerçekleştirmek için doğrudan tarayıcıyı kullanabiliriz. Basit bir HTTP GET ile gönderdiğim parametreyi almayı başardım. Dikkat ettiyseniz Authorization olarak Function level'ı seçmiş olsak da localde bu yapı çalışmıyor. Azure Functions'ı deploy ettiğimizde bu functiona ulaşmak için ek olarak Azure'dan alacağımız anahtarı kullanmamız gerekecek.

* Deployment

Gelin son olarak, hızlıca bu function'ı bir deploy edelim. Projeye sağ tuş tıklayıp "Publish" dediğinizde "Microsoft Azure App Service"i seçmeniz gerekiyor.

Azure Functions Deployment

Bir sonraki adımda klasik Azure Deployment hikayelerinden biri ile karşı karşıyayız. Ama burada dikkat etmeniz gereken bir şey var. Eğer varsayılan ayarlarla deployment yaparsanız sihirbaz size "App Service Plan" yaratırken "Free Tier" veya "Shared Plan" kullanamayacağınıza dair uyarılar verecektir. Aslında yapmak istediğimiz şey bu yazının başında da bahsettiğim gibi "Consumption Plan"ı kullanmak.

Azure Functions Deployment

Bunu yapabilmek için "App Service Plan"ın yanında "New" düğmesine basarak "Size" olarak Consumption seçmeniz gerekiyor. Bu plan sadece Function deploy etmek için kullanılabiliyor. Zaten bizim de başka bir şey deploy etme planımız yok. Fakat eğer sizin hali hazırda App Service Plan'larınız var ve o planlarda olan diğer uygulamalar ile Azure Functions'ın ortak çalışmasını istiyorsanız tabi ki diğer planlarınızı da seçebilirsiniz. Tabi böyle bir tercih yaptığınızda artık kendi seçtiğiniz VM Set'de çalışıyorsunuz demektir ve fiyatlandırması da ona göre olacaktır. Yani özetle, "Consumption Plan"da değilsiniz aslında "Serverless" falan da değilsiniz demektir :) Sihirbazın bundan sonrası artık bir "Next, next, next" hikayesi.

Deployment sonrasında Azure Portal'ına giderseniz Function'ı görebilir ve erişim için gerekli olan Authorization Key'de Functions Key altından ulaşabilirsiniz.

Azure Functions Azure Portal'ında.

Yukarıdaki ekran görüntüsünde sağ tarafta yazdığımız Function'ı, sol tarafta gerekli Authorization Key'i ve ortada da Visual Studio'da yazdığımız kodu bulabilirsiniz.

İlk Serverless maceramız hepimize hayırlı olsun ;) Görüşmek üzere.

Bu yazıyı yazmam 42 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 3 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Hatırlarsanız geçen ay epey sakin geçmişti. Bu ay ise tam tersine, inanılmaz yoğun bir ay oldu. Özellikle Microsoft Connect etkinliği ile beraber duyuruların sonu gelmedi. Her zamanki gibi yine hızlı bir liste yapalım, sonra da birkaç tane duyurunun detaylarına göz atarız.

Liste uzun demiştim. Geçen ayki sessizlikten sonra bu ay okuyacak, karıştıracak, deneyecek çok şey var. Aşağıda birkaç ana başlığı ayrıca sizler için toplamaya çalıştım. Bol haberli bu ayın tadını çıkartın. Görüşürüz.

Daron Yöndem
@daronyondem
MVP, Microsoft Regional Director

Visual Studio for Mac!

Sanırım bu haberin başında uzun bir es vermem gerekiyor. Derin bir nefes alıp, haberi sindirmeye çalışmak gerek. Microsoft’un artık Mac ortamı için bir Visual Studio sürümü var. İşin özüne inecek olursak Xamarin Studio adını değiştirdi ve üzerine de .NET Core ile back-end API development özellikleri geldi diyebiliriz. Zamanla Razor gibi view engine’lerin de VS for Mac’e geleceğinden bahsediliyor. Detayları merak edenler buraya bir göz atabilirler.

Visual Studio 2017 RC çıktı!

Visual Studio’nun yeni sürümüne çok yaklaştım. RC sürümündeki yenilikler arasında dikkati çekenler ilk aşamada yazılım geliştirici üretkenliğini arttırma amaçlı özellikler oluyor. Bir adım daha ileri gittiğinizde VS 2017 RC ile beraber birçok yeni toolkit’in de geldiğini görebilirsiniz. Bunlardan en popüleri belki de Docker Toolkit. Visual Studio 2017 RC yeniliklerine hızlıca bakmak isteyenler bu yazıyı okuyabilirler.

Linux üzerinde SQL Server Preview oldu.

SQL Server’ın Linux ortamına gelmesi ile ilgili haberler ilk günden beridir büyük şaşkınlık yaratıyor. Oysa .NET Core ile artık uygulamaların platform bağımsız olduğunu düşünürseniz beraberlerinde bir veritabanı motorunun da bulunması çok önemli. O nedenle SQL Server’ın Linux’e taşınıyor olması .NET Core geliştiricileri için büyük değere sahip. SQL Server’ın Linux tarafındaki Preview haline ulaşmak için buradan ilerleyebilirsiniz.

Visual Studio Team Foundation Server 2017 RTM oldu.

Özellikle kendi sunucularında TFS kullananlar için 2017 sürümünün RTM olması çok değerli. Package Management tarafında artık Private Nuger feedleri oluşturabilirsiniz. Git tarafında birçok yeni özellik geliyor, squash merge, iterative reviews bunlardan birkaçı. Java build templates, Xamarin build tasks, Docker desteği… Daha fazlasını merak edenler hemen bu linkten devam edebilirler.

Bu yazıyı yazmam 12 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 1 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Dün akşam Microsoft binasında re-Connect 2016 etkinliğini gerçekleştirdik. Etkinliğin başında tüm konuşmacılar olarak esas Connect etkinliğinden edindiğimiz izlenimleri paylaştıktan sonra teknik oturumlara geçtik. Ben de en sonda Azure Yenilikleri'nden bahsettim. Azure Container Services ile DC/OS ve Visual Studio + Docker entegrasyonuna göz attık. Sonrasında Azure Functions'a şöyle bir bakıp, DocumentDB'nin de local emülatörünü gördük. 30 dakikalık oturumumu 45 dakikaya uzatarak ancak bu kadarını sığdırabildim :)

reConnect 2016'da açılış konuşması.

Hafta içi bir akşamda bizleri yalnız bırakmayan herkese teşekkürler. Bir sonrakinde görüşmek üzere ;)

Twitter
RSS
Youtube
RSS Blog Search
Arşiv'de tüm yazıların listesi var. Yine de blog'da arama yapmak istersen tıkla!
Instagram Instagram