Lucene net 3.0.3

Полтора месяца назад доступна стала обновленная версия Lucene 3.0, а значит команда, которая полтора года назад забросила развитие этого проекта получила новое финансирование. В связи с этим была обновлена и моя поисковая система. Замечено заметное улучшение скорости индексирования при ограниченных ресурсах. Примерно в 2-2,5 раза. То есть даже при минимальных мощностях можно индексировать по 300 постов в секунду. Обусловлено прежде всего тем, что анализаторы в новой библиотеки оперируют не токенами, а байтами и буферами, что сильно ускоряет токенизацию. Также в связи с новой библиотекой потребуется обновить Azure Directory, но там ничего сложного. Общая концепция изменения объекта Directory в том, что объединили функции Close и Dispose и во многих местах ушли от функций в пользу get-property что уменьшает расход памяти.

AzureDirectory.rar (1,58 mb)


Метки:   Категории:microsoft | Code | t30p


Контекстный поиск с русской и английской морфологией на основе проектов с открытым исходным кодом и облачных сервисов Microsoft Azure

keywords: контекстный поиск, русская морфология, поточное индексирование, облачные сервисы
abstract: В статье дается общее представление об архитектуре поискового сервиса, построенного на проектах с открытым исходным кодом и рассматриваются базовые проблемы реализации безотказной его работы в условиях поточных данных и минимальных системных ресурсах. Реализация опирается на следующие проекты:

На скриншотах присутствует программа для работы с BlobStorage - AzureStorageExplorer, которая также доступна с открытым кодом - http://azurestorageexplorer.codeplex.com/. Сам пост написан и хостится в облаке Azure с использованием BlogEngine - http://blogengine.codeplex.com/

 

Общее

Разложим задачу создания поискового сервиса на пять составляющих: 1. Сбор данных. 2. Фильтрация, токенизация, стемминг, синонимизация данных. 3. Помещение в поисковый индекс. 4. Обработка поисковых запросов. 5. Распознавание что хотел пользователь и предоставление нативного языка запросов.

В сегодняшней статье не будут разобраны пункты 2 и 5. Так как токенизация и стемминг осуществляется при помощи библиотеки Lemmatizer.dll, разработанной сотрудниками Яндекса, и рассматривалась мною 4 года назад. За прошедшее время статью открывали более 20 тыс. раз. Сама же библиотека Lemmatizer.dll перестала быть платной и с 2011 года доступна всем желающим в исходниках. Последний пункт - создание человеческого языка, запросов требует большую наработку данных уже от пользователей производящих запросы, и может дорабатываться в процессе работы поискового сервиса. На сегодняшний момент языком запросов будет являться стандартный синтаксис Lucene, который достаточно гибкий. Так например, орфографические ошибки в запросах можно исправлять с помощью нечеткого поиска "word~",

Сбор данных

В поточном режиме при помощи Twitterizer.NET подключимся к Twitter.Streaming API. Вытаскиваем все русскоязычные твиты, фотографии instagram, видео с youtube (+Vimeo), посты на стене Вконтакте, фотографии Facebook, посты GooglePlus. Для разбора данных воспользуемся библиотеками BlogsAPI, GoogleGData, FacebookSDK соответственно. Вытаскиваем из соц.сетей все данные, которые не требуют access_token - то есть присутствия пользователя. Примеры извлечения этих данных есть в примерах самих библиотек. Также важно понимать, что большинство ссылок на информацию пропущено через сервисы сокращения ссылок. Для разворачивания таких ссылок есть простая функция в BlogsAPI для большинства известных сокращателей:

Copy Source | Copy HTML
  1. Shortener shrt = Common.GetShortenerByLink(url.ExpandedUrl) as Shortener;
  2. if (shrt != null)
  3. {
  4.         fulllink = shrt.ConvertDataTo(url.ExpandedUrl, ItemType.ShortUrl, ItemType.FullUrl);
  5. }

После извлечения - данные группируются в пакеты (Batch) и отправляются на WCF-сервис индексирования. Для сложных данных, требующих дополнительных запросов используется Azure BlobQueue(примеры работы) для отложенной индексации или, как в случае Facebook и Vkontakte, группового извлечения данных, когда в одном запросе к социальной сети запрашивается сразу несколько постов.

Так как много данных не бывает, то в тотже индекс мы добавим все посты (если быть точным - большую часть) livejournal.com, liveinternet.ru, qip.ru,juick.com, blogi.mail.ru и т.д. Отдельно отмечу, что такой интернет гигант как Яндекс не выдает информации по своим блогам wow.ya.ru и видеоблогам на video.yandex.ru через API.

Индексирование

Благодаря облачным технологиям Микрософта у нас появляется замечательный объект BlobStorage, который предоставляет до 100 ТБ под хранение индекса. Чтобы разместить в нем наш поисковый индекс формируемый Lucene, достаточно воспользоваться проектом AzureDirectory. Основной индекс будет храниться в BlobStorage, а на локальном инстансе WebRole создается специальная промежуточная директория - LocalStorage, где производятся все операции с индексом по поиску и изменению его. Важно понимать, что инстансов WebRole у нас должно быть 2 и более, так как к конечный момент времени один из них заблокировал индекс на запись (добавление объектов), а с остальных идет непрерывное чтение (поисковые команды). При этом чтение на инстансе, где идет запись - невозможно. Модель "один писатель, много читателей". При завершении записи данные с локального LocalStorage копируются в центральный BlobStorage откуда синхронизируются на остальные машины. Ниже приводится класс, который реализует связь Lucene и AzureDirectory, функции работы с Lucene не приводятся, примеры этого есть в интернете.

  1. public abstract class IndexerBase
  2. {
  3.     protected IndexerBase()
  4.     {
  5.     }
  6.     /// <summary>
  7.     /// Constructor
  8.     /// </summary>
  9.     protected IndexerBase(string sDir):base()
  10.     {
  11.         IndexDir = sDir;
  12.     }
  13.     /// <summary>
  14.     /// Объект блокировки, переопределяемый дочерними классами
  15.     /// </summary>
  16.     protected static object oWriting = new object();
  17.     /// <summary>
  18.     /// Название папки с индексом
  19.     /// </summary>
  20.     protected string IndexDir = "IndexBase";
  21.     public string cachepath
  22.     {
  23.         get
  24.         {
  25. #if DEBUG
  26.             return Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), IndexDir);
  27. #else
  28.             return Path.Combine(RoleEnvironment.GetLocalResource("MyStorage").RootPath,IndexDir);
  29. #endif
  30.         }
  31.     }
  32.     private AzureDirectory _azureDirectory;
  33.     /// <summary>
  34.     /// Директория
  35.     /// </summary>
  36.     protected AzureDirectory azureDirectory
  37.     {
  38.         get
  39.         {
  40.             if (_azureDirectory != null) return _azureDirectory;
  41.             _azureDirectory = new AzureDirectory(CloudStorageAccount.Parse(
  42.                         RoleEnvironment.GetConfigurationSettingValue("SearchConnectionString")), IndexDir,
  43.                         new SimpleFSDirectory(new DirectoryInfo(cachepath)));
  44.             return _azureDirectory;
  45.         }
  46.     }
  47.     /// <summary>
  48.     /// Указывает на первое время запуска и открытия поискового индекса
  49.     /// </summary>
  50.     protected static Boolean firsttime = true;
  51.     /// <summary>
  52.     /// Статический поток занимающийся перезаписью сегментов
  53.     /// </summary>
  54.     protected static ConcurrentMergeScheduler Merger = new ConcurrentMergeScheduler();
  55.     /// <summary>
  56.     /// То, как писать
  57.     /// </summary>
  58.     private IndexWriter _MyWriter = null;
  59.     protected IndexWriter MyWriter
  60.     {
  61.         get
  62.         {
  63.             if (_MyWriter != null) return _MyWriter;
  64.             lock (oWriting)
  65.             {
  66.                 if (_MyWriter != null) return _MyWriter;
  67.                 CreateMyWriter();
  68.             }
  69.             return _MyWriter;
  70.         }
  71.         set { _MyWriter = value; }
  72.     }
  73.     private void CreateMyWriter()
  74.     {
  75.         try
  76.         {
  77.             _MyWriter = new IndexWriter(azureDirectory, MyAnalyser, false);
  78.             //важны атомарные операции, так как несмотря на lock(oWriting) в случае нескольких дочерних классов индексирования, возникнуть может многопоточность
  79.             Interlocked.Exchange(ref LockFailsCounter,  0); //успешно открылось на запись, сбросили счетчик неудач
  80.         }
  81.         catch (LockObtainFailedException e0)
  82.         {
  83.             //если уже много ошибок подряд, около часа
  84.             if (LockFailsCounter > 30)
  85.             {
  86.                 //принудительно удаляем блокировку =(
  87.                 azureDirectory.ClearLock("write.lock");
  88.                 //Trace.Write(e0);
  89.                 Trace.Write("Принудительное снятие старой блокировки на запись.");
  90.                 Thread.Sleep(60 * 1000); //1 min for unlock
  91.                 _MyWriter = new IndexWriter(azureDirectory, MyAnalyser, false);
  92.                 Interlocked.Exchange(ref LockFailsCounter,  0);//успешно открылось на запись, сбросили счетчик неудач
  93.             }
  94.             else
  95.             {
  96.                 Interlocked.Increment(ref LockFailsCounter);
  97.                 throw;
  98.                 //вызовем ошибку, чтобы запрос на индексирование был повторен через некоторое время, когда файл разблокируется
  99.             }
  100.         }
  101.         catch (Exception err)
  102.         {
  103.             Trace.WriteLine("Не удалось открыть старый индекс: " + err.Message);
  104.             if (System.IO.Directory.Exists(cachepath))
  105.             {
  106.                 System.IO.Directory.Delete(cachepath, true);
  107.                 //удаляем возможный старый хлам помешавший открытию
  108.                 System.IO.Directory.CreateDirectory(cachepath);
  109.             }
  110.             //создадим новый еще раз!
  111.             _MyWriter = new IndexWriter(azureDirectory, MyAnalyser, true);
  112.         }
  113.         _MyWriter.SetMergeScheduler(Merger);
  114.         _MyWriter.SetMergeFactor(10);
  115.         //_MyWriter.SetUseCompoundFile(false);
  116.     }
  117.     /// <summary>
  118.     /// Время открытия поискового индекса
  119.     /// </summary>
  120.     protected DateTime? SyncTime
  121.     {
  122.         get { return HttpContext.Current.Application[GetType() + "synctime"] as DateTime?; }
  123.         set { HttpContext.Current.Application[GetType() + "synctime"] = value; }
  124.     }
  125.     /// <summary>
  126.     /// Количество неудачных попыток открыть индекс на запись
  127.     /// </summary>
  128.     /// <remarks>Важно, что это один объект на все дочерние классы индексирования</remarks>
  129.     private static int LockFailsCounter =  0;
  130.     /// <summary>
  131.     /// То, как писать
  132.     /// </summary>
  133.     protected IndexSearcher _MySearcher
  134.     {
  135.         get { return HttpContext.Current.Application[GetType() + "searcher"] as IndexSearcher; }
  136.         set { HttpContext.Current.Application[GetType() + "searcher"] = value; }
  137.     }
  138.     public IndexSearcher MySearcher
  139.     {
  140.         get
  141.         {
  142.             if (SyncTime.HasValue && SyncTime.Value.AddHours(1)<DateTime.Now)//более ~  с момента последней синхронизации(!)
  143.             {
  144.                 //делаем запись только в том, случае, если индекс уже был успешно открыт ранее на запись, и сделана синхронизация с основным
  145.                 //иначе может произойти потеря
  146.                 IndexCommit();
  147.             }
  148.             if (_MySearcher != null) return _MySearcher;
  149.             lock (oWriting)
  150.             {
  151.                 if (_MySearcher != null) return _MySearcher;
  152.                 var myPerf = new PerformanceTimer();
  153.                 myPerf.StartTimer();
  154.                 try
  155.                 {
  156.                     _MySearcher = new IndexSearcher(azureDirectory, true);
  157.                 }
  158.                 catch (CorruptIndexException)
  159.                 {
  160.                     //требуется починка индекса
  161.                     FixIndex(RoleEnvironment.DeploymentId);
  162.                 }
  163.                 catch (Exception e1) //если у нас нету еще директории индекса или сбой соединения
  164.                 {
  165.                     Trace.Write(e1);
  166.                     if (_MyWriter != null)
  167.                     {
  168.                         Trace.Write("Warning: have to close MyWriter to open MySearcher!");
  169.                         _MyWriter.Commit();
  170.                         _MyWriter.Close();
  171.                         _MyWriter = null;
  172.                     }
  173.                     if (System.IO.Directory.Exists(cachepath))
  174.                     {
  175.                         System.IO.Directory.Delete(cachepath, true); //удаляем возможный старый хлам помешавший открытию
  176.                         System.IO.Directory.CreateDirectory(cachepath);
  177.                     }
  178.                     try
  179.                    &nbssearcherp;{
  180.   nbsp;запись                      //важно для всех случаев когда индекс открывается в отсутствующую папку.
  181.                         var openindex = MyWriter;
  182.                     }catch(LockObtainFailedException)
  183.                     {
  184.                         //Папка с индексом успешно открыта и создана, однако другая роль уже пишет индекс
  185.                     }
  186.                     try
  187.                     {
  188.                         _MySearcher = new IndexSearcher(azureDirectory, true);
  189.                     }catch(FileNotFoundException foi)
  190.                     {
  191.                         //индекс скопированный с мастера не содержит нужного сегмента
  192.                         if(foi.InnerException!=null && foi.InnerException.GetType() == typeof(StorageClientException))
  193.                         {
  194.                             //вызываем утилиту исправления ошибок
  195.                             FixIndex(RoleEnvironment.DeploymentId);
  196.                         }
  197.                     }
  198.                     Trace.Write(String.Format("BadMode: _MySearcher {0} loaded in {1}", GetType().Name, myPerf.StopTimer()));
  199.                 }
  200.                 if(firsttime)
  201.                 {
  202.                     firsttime = false;
  203.                     Trace.Write(String.Format("GoodMode: _MySearcher {0} loaded in {1}", GetType().Name, myPerf.StopTimer()));
  204.                 }
  205.             }
  206.             return _MySearcher;
  207.         }
  208.     }
  209.     /// <summary>
  210.     /// Морфологический анализатор
  211.     /// </summary>
  212.     internal static MorphologyAnalyzer _MyAnalyser;
  213.     protected MorphologyAnalyzer MyAnalyser
  214.     {
  215.         get
  216.         {
  217.             if (_MyAnalyser != null && _MyAnalyser.Morph != null && _MyAnalyser.Morph.isLoaded) return _MyAnalyser;
  218.             Trace.Write("Заново загружаем MorphologyAnalyzer");
  219.             _MyAnalyser = new MorphologyAnalyzer(null, false);
  220.             return _MyAnalyser;
  221.         }
  222.     }
  223.     static IndexerBase()
  224.     {
  225.         RoleEnvironment.Stopping += delegate
  226.         {
  227.             try
  228.             {
  229.                 //дожидаемся завершения записи
  230.                 lock (oWriting)
  231.                 {
  232.                     //останавливаем поток слияния
  233.                     Merger.Close();
  234.                     Merger = null;
  235.                 }
  236.             }catch(Exception e)
  237.             {
  238.                 Trace.Write(e);
  239.             }
  240.         };
  241.     }
  242.     /// <summary>
  243.     /// Запись индекса на диск и переоткрытие поискового механизма
  244.     /// </summary>
  245.     protected void IndexCommit()
  246.     {
  247.         SyncTime = DateTime.Now;
  248.         lock (oWriting)
  249.         {
  250.             if (_MyWriter != null)
  251.             {
  252.                 //закрыли текущий коммит
  253.                 try
  254.                 {
  255.                     _MyWriter.Commit();
  256.                 }catch(CorruptIndexException)
  257.                 {
  258.                     FixIndex(RoleEnvironment.DeploymentId);
  259.                 }
  260.                 _MyWriter.Close();
  261.                 _MyWriter = null;
  262.             }
  263.             if (_MySearcher != null)
  264.             {
  265.                 //закрыли тукущий поисковик
  266.                 _MySearcher.Close();
  267.                 _MySearcher = null;
  268.             }
  269.         }
  270.         //открыли новый поисковик
  271.         var opensearcher = MySearcher;
  272.     }
  273.     /// <summary>
  274.     /// Серьезный процесс по востановлению индекса 
  275.     /// </summary>
  276.     /// <param name="DeploymentId"></param>
  277.     /// <returns></returns>
  278.     public string FixIndex(string DeploymentId)
  279.     {
  280.         var myPerf = new PerformanceTimer();
  281.         myPerf.StartTimer();
  282.         Trace.WriteLine(String.Format("Вызов проверки статуса и исправления индекса со значением  {0} / {1}", DeploymentId, GetType().Name));
  283.         string lockfile = GetType().Name + "_" + DeploymentId + ".lock";
  284.         Lock indexLock = azureDirectory.MakeLock(lockfile);
  285.         if (indexLock.Obtain())//блокировка основного индекса
  286.         {
  287.             try
  288.             {
  289.                 //устроим проверку индекса!
  290.                 CheckIndex fixer = new CheckIndex(azureDirectory);
  291.                 CheckIndex.Status oStatus = fixer.CheckIndex_Renamed_Method();
  292.                 Trace.WriteLine(GetType().Name+":status=" + oStatus.numBadSegments + ":" + oStatus.totLoseDocCount);
  293.                 if (oStatus.numBadSegments != oStatus.numSegments && oStatus.numBadSegments >  0)
  294.                 {
  295.                     fixer.FixIndex(oStatus); //опасная функция перезаписи
  296.                 }
  297.                 Trace.WriteLine(GetType().Name + ":FixIndex finished");
  298.                 //убедимся, что разблокировали индекс после правки
  299.                 azureDirectory.ClearLock("write.lock");
  300.             }
  301.             catch (Exception e1)
  302.             {
  303.                 Trace.Write(e1);
  304.             }
  305.             finally
  306.             {
  307.                 indexLock.Release();
  308.                 azureDirectory.DeleteFile(lockfile);
  309.             }
  310.             //////////******************////////////
  311.         }
  312.         return myPerf.StopTimer().ToString();
  313.     }
  314. }


Поясню основные моменты:
1. Класс абстрактный, так как от него наследуется WCF сервис, который может содержать свой индекс. То есть один инстанс может работать с несколькими индексами Lucene которые хранятся в одном BlobStorage в разных папках.
2. В Azure любой инстанс может быть выключен в любую секунду, поэтому важно обрабатывать событие остановки и корректно завершать работу отдельного потока ConcurrentMergeScheduler занимающегося оптимизацией индекса.
3. Объект чтения MySearcher у нас открыт всегда, чтобы минимизировать время поиска. Объект записи MyWriter создается только при получении данных на запись и соответственно закрывает и блокирует чтение для текущего индекса на инстансе.
4. Очень важна отказоустойчивость, поэтому такая детальная обработка ошибок LockObtainFailedException - когда пришли данные на запись, а основной индекс в BlogStorage заблокирован. Например, другим инстансом, который после этого падает по OutOfMemoryException и не снимает блокировки. Соответственно через несколько таких ошибок блокировка снимается автоматически и буфера поставщиков данных не успевают переполниться и потери данных не происходит.
5. Другая стандартная ошибка - CorruptIndexException - вполне может быть следствием сбоем сети при передачи больших файлов. При этом последняя операция записи отменяется, однако это занимает значительное время и блокирует другие операции записи. Ниже, как это выглядит в логах.




Еще раз перечислю основные проблемы, к которым нужно быть готовыми: одновременный приход запросов на индексирование, произвольное выключение инстанса, нечитаемость индекса, нехватка памяти, невозможность эксклюзивно заблокировать индекс, очищение LocalStorage. Последние подразумевает то, что согласно документации, подключаемый к инстансу диск несмотря на настройку cleanOnRecycle="false" всеравно не является величиной постоянной и может быть очищен в любой момент. Последнее означает повторную синхронизацию с центральным BlobStorage и открытие объектов чтения и записи займет дольше обычного (2-3 сек) на несколько минут. В итоге все работает стабильно и загрузка одного индексирующего инстанса показана ниже. К слову, мы используем всего-лишь два инстанса с минимальными мощностями, так как сильно ограничены в ресурсах. Пикам по 100% соответствует обработка пакета на индексацию. С уверенностью можно сказать, что за простой ресурсов мы не платим.


Вторая картинка приведена для сравнения, и формируется на системном портале Azure как среднее по WebRoles в Endpoint. Очевидно, что она не дает представления о том, что твориться с инстансами.

Выполнение поисковых запросов

В условиях, когда мы не знаем в каком состоянии находится инстанс, пишет он или нет, единственный правильный вариант - отправлять поисковый запрос на все инстансы, и брать первый ненулевой ответ. При наличии ресурсов можно было бы выделить отдельную Endpoint и WebRole, которая работала бы только на чтение. Но мы не ищем простых путей, поэтому на каждой webrole с индексом имеется следующая поисковая функция, выполняющаяся только если инстанс не пишет в индекс

Copy Source | Copy HTML
  1. /// <summary>
  2. /// Контекстный поиск с проверкой на поток записи, если открыта запись, то возвращаем NULL
  3. /// </summary>
  4. /// <param name="opts">Слова и параметры поиска</param>
  5. /// <param name="ignorelock">Не проверять идет ли запись</param>
  6. /// <returns>найденные результаты, CAN BE NULL</returns>
  7. public IndexedDocument[] SearchForAdvanced(SearchForOptions opts, bool ignorelock)
  8. {
  9.     //без проверки на блокировку
  10.     if (ignorelock) return SearchFor(opts);
  11.     bool isNotLocked = false;
  12.     try
  13.     {
  14.         isNotLocked = Monitor.TryEnter(oWriting);
  15.     }
  16.     finally
  17.     {
  18.         if (isNotLocked)
  19.         {
  20.             //важно освободить блокировку на поиск как можно скорее
  21.             Monitor.Exit(oWriting);
  22.         }
  23.     }
  24.     if (isNotLocked)
  25.     {
  26.         return SearchFor(opts);
  27.     }
  28.     //заблокировано
  29.     return null;
  30. }

И соответственно на поисковом клиенте реализуется многопоточная отправка с ожидаем быстрейшего ненулевого результата через ThreadPool.QueueUserWorkItem(state => SearchForAdvancedAsync(oData));

Copy Source | Copy HTML
  1. /// <summary>
  2. /// Объет параллельного запроса
  3. /// </summary>
  4. public class SearchAsyncObject
  5. {
  6.     /// <summary>
  7.     /// Число параллельных запросов
  8.     /// </summary>
  9.     public readonly int Count = 3;
  10.     /// <summary>
  11.     /// Число обработанных вызовов
  12.     /// </summary>
  13.     public int Finished =  0;
  14.     /// <summary>
  15.     /// Поисковые параметры
  16.     /// </summary>
  17.     public SearchForOptions opts = null;
  18.     /// <summary>
  19.     /// Результат поиска
  20.     /// </summary>
  21.     public IndexedDocument[] Founds = null;
  22.     /// <summary>
  23.     /// Mutex
  24.     /// </summary>
  25.     public ManualResetEvent mre = new ManualResetEvent(false);
  26. }
  27. /// <summary>
  28. /// Функция завершения поиска
  29. /// </summary>
  30. private static void SearchForAdvancedAsync(SearchAsyncObject data)
  31. {
  32.     try
  33.     {
  34.         IndexClient service = Indexer.GetIndexClient();
  35.         IndexedDocument[] founds = service.SearchForAdvanced(data.opts, false);
  36.         if (founds != null && data.Founds == null) //найден результат(!)
  37.         {
  38.             data.Founds = founds;
  39.             data.mre.Set();
  40.         }else if (Interlocked.Increment(ref data.Finished) >= data.Count){
  41.             data.mre.Set();//все потоки завершили работу
  42.         }
  43.     }
  44.     catch (Exception e1)
  45.     {
  46.         Trace.Write(e1);
  47.     }
  48. }

Функции добавления документов в индекс и выполнение операций поиска уже на конкретной машине выполняются как описано в документации Lucene.

 

Результат

Построенный поисковый сервис используется для выявления наиболее интересной информации в русскоязычных социальных сетях и ранжирования этой информации для формирования топа событий на сайте http://t30p.ru. Полученное таким образом самостоятельное независимое СМИ сильно снижает стоимость и увеличивает качество интернет журналистики. В поисковый индекс можно добавлять новые индексные поля и обновлять код без нарушения работы сервиса или потери данных (проверено). Сейчас в индексе более 200 млн. объектов от 2,5 млн. социальных аккаунтов; примерно 2,5 млн. новых объектов в сутки; размер индекса в пределах 100 GB; время выполнения запросов - 0,5 сек; к индексу выполняется примерно 1000 запросов в час, в том числе внутре-технические. Ожидаемое время запаздывания между нахождением данных и появлением их в поисковой выдаче - 5-15 мин. (в частности, задержку дает "кэширующий" CDN на поисковой странице).

Проблемы и решения

Более int.MaxValue объектов
Поисковый индекс Lucene ограничен примерно в 2 млрд. объектов. Поэтому для работы с большим числом объектов на помощь приходит MultiSeacher, который еще не портирован в .Net версию. Он позволяет производить поиск по конечному набору поисковых индексов при этом теряется скорость.
Нехватка места или превышение 100ТБ
При использовании MultiSeacher для поиска по нескольким поисковым индексам каждый индекс может быть привязан к своему диску или BlobStorage, что решает проблему с ограничением места.
Дублирование информации
Несмотря на обработку поточных данных от самих социальных сервисов, в них часто происходят сбои и разрывы соединений, что приводит к разного рода ошибкам. Так возможно повторное индексирование постов. Поэтому на поисковом клиенте производится проверка уникальности ссылки на пост в результате. В теории эту задачу можно переложить на фильтр DuplicateFilter
Lucene 4.0 имеет классы встроенной русскоязычной морфологии
Какой бы не была встроенная морфология, я бы рекомендовал использовать более тяжеловесный Lemmatizer, библиотеки которого в архиве zip занимают 100МБ, что говорит о серьезности подхода к составлению словаря.
И куда потом все эти собранные данные?
Например, можно создать сервис в Azure Marketplace и продавать доступ к ним по фиксированной цене. Там же есть интересный платный сервис перевода текстов на многие языки, что позволит в автоматическом режиме перевести всю собранную информацию на любой язык и сделать международный топ русскоязычных социальных новостей.


Метки: , , , , , ,   Категории:Blogs | Csharp | Yandex | microsoft | Code


.NET 5 Namespaces

Небольшая дискуссия развернулась среди передовых разработчиков .Net . Был поднят вопрос о целесообразности использования пространств имен (Namespaces) в проектах или это уже устаревшая традиция. Как пример успешной технологии без пространств имен приводят jQuery. К слову идею отказа от namespaces поддержал Mads - один из основных разработчиков MSVS2012 и плагинов работы со стилями. В моем же понимании пространста имен должны остаться, например когда к проекту подключается сторонний сервис у которого есть классы с такими же именами. И в целом проставление правильных пространств имен не создает проблем разработчику, если у него в MSVS стоит Resharper позволяющий делать это за один клик.

Метки:   Категории:Code


Emoji символы

Одна из новостей сегодняшних трендов твиттера - вконтактик добавил отображение эмоциональных значков для пользователей мобильной версии. Отмечу, что топ умеет их отображать достаточно давно для любого браузера, а не пустые квадратики, как это сделано в твиттере. Проблема этих значков в том, что 1) их много 2) они представляют из себя UTF-32 символы, в то время как большинство браузеров работает в UTF-8, а сама MSVS хоть и называет кодировку utf-8, всеравно все строки обрабатывает как UTF-16. А вот телефон, типа iphone обрабатывает все как utf-32 , поэтому у него много возможностей по добавлению дополнительных символов.

Итого, чтобы на своем сайте отобразить emoji символы достаточно 1) сделать ссылку на CSS - http://cdn.t30p.ru/t30p.ru/emoji.css , 2) При разботе строчки текста в utf-16 побайтово определять верхние и нижние суррогатные символы, из 2х таких utf-16 байтов как раз и формируется utf-32 emoji символ. 3) Получить шестизначный код utf-32 символа из двух utf-16 байтов, для этого нам помогает функция Char.ConvertToUtf32. Итак, ниже пример функции, которая находит и заменяет в строчке суррогаты на нужные SPAN.
Copy Source | Copy HTML
  1. public static string DetectSurrogates(string text)
  2.     {
  3.         StringBuilder sbOutput = new StringBuilder();
  4.         char ch;
  5.         for (int i =  0; i < text.Length; i++)
  6.         {
  7.             ch = text[i];
  8.             if ((ch >= 0x0020 && ch <= 0xD7FF) ||
  9.                 (ch >= 0xE000 && ch <= 0xFFFD) ||
  10.                 ch == 0x0009 ||
  11.                 ch == 0x000A ||
  12.                 ch == 0x000D)
  13.             {
  14.                 sbOutput.Append(ch);
  15.             }
  16.             else
  17.             {
  18.                 sbOutput.Append(String.Format("<span class=\"emoji emoji{0:x}\"></span>", Char.ConvertToUtf32(ch, text[i + 1])));
  19.                 i++;//пропускаем 1 байт
  20.             }
  21.         }
  22.         return sbOutput.ToString();
  23.     }


Метки:   Категории:Code


Instagram limits

Инстаграм относится к таким компаниям, которые не имеют нормального API (например не представлена библиотеки на .Net кроме как BlogsAPI). Одно из последних нововведений, которые они успели сделать - это сильно ограничить использование своего API, при этом не отразили это в документации. Поэтому выношу сюда информацию из обсуждения в гугл-группе.
to prevent the bot program, instagram must use restriction request. I'm concerned, the limit to like 500 / hour, the limit for comment 60 / hour, and the limit to follow 200 / hour.

Метки:   Категории:Code


Немного про UTF-16 и отличие от UTF-8

Как мы знаем, все строчки в коде msvs обрабатываются как utf-16, хотя формально мы их называем как utf-8. Отличие заключается в нижних и верхних суррогатах, которые не понятно вообще зачем нужны. Проблема может возникнуть при индексировании разных текстов, когда идет вызов WCF сервиса и ему в параметре передается UTF-16 строчка, а сам сервис должен передавать в utf-8, поэтому найдя символ из utf-16 все успешно проваливается с сообщением - Unable to translate Unicode character \uDE09 at index 0 to specified code page. Один из сложных и рекомендуемых способов - переопределить кодировщик у WCF сервиса, однако можно и ручками предварительно перекодировать строчку, чтобы быть уверенным, что она содержит только utf-8 коды.
Copy Source | Copy HTML
  1. private static readonly Encoding Utf8Encoder = Encoding.GetEncoding(
  2.                 "UTF-8",
  3.                 new EncoderReplacementFallback(string.Empty),
  4.                 new DecoderExceptionFallback()
  5. );
  6. text = Utf8Encoder.GetString(Utf8Encoder.GetBytes(text""));

Метки:   Категории:Code


Библиотека для работы с SAPE и хранением данных в Azure Table Storage

По ссылке можно скачать проект библиотеки (исходники и бинарники) для биржи sape.ru . Хранение ссылок производиться в облачном BlogStorage, чтобы позволяет использовать библиотеку на любом проекте ASP.NET, в том числе и с несколькими WebRole's. Ключ доступа к таблице хранения задается в "Diagnostics.ConnectionString". Далее в конфиг прописываем секцию:
Copy Source | Copy HTML
  1. <configSections>
  2. <sectionGroup name="Sape">
  3.       <section name="SapeConfig" type="Sape.SapeConfigSection, SAPE" allowLocation="true" requirePermission="false" allowDefinition="Everywhere" />
  4.     </sectionGroup>
  5.   </configSections>
  6. <Sape>
  7.     <SapeConfig UserId="111111111121a924a9cd073ff1d2b0da">
  8.       <DefaultDocuments>
  9.         <add Name="Default.aspx" />
  10.       </DefaultDocuments>
  11.     </SapeConfig>
  12.   </Sape>
И далее можем вставлять в любое место проекта ссылки сапы. Работает, как можно видеть только для ссылок, без контекстных ссылок:
Copy Source | Copy HTML
  1. <pages enableSessionState="false" enableViewStateMac="false" enableEventValidation="true" controlRenderingCompatibilityVersion="3.5" clientIDMode="AutoID">
  2.       <controls>
  3.         <add tagPrefix="efe" namespace="Sape" assembly="SAPE" />
  4.       </controls>
  5. </pages>
  6. <efe:SapeLinks runat="server"/>

Метки:   Категории:Code


Instagram-oauth-2

Сегодня не работает Инстаграм, поэтому небольшой фрагмент кода рассказывающий про реализацию oauth2.0 для instagram. Проект этот достаточно молодой, поэтому наплевал не только на интерфейс, но и на правильность реализации некоторых функций, в частности в документации есть ошибки, где указан параметр client_id вместо consumer_key, надо быть внимательным. Но основную проблему может создать некорректность ответа на запрос о получении токена, со стороны инстаграма. И так как мы используем библиотеку DotNetOpenAuth для реализации залогинивания, то и переопределение или замена каких-либо методов достаточно сложна. Во-первых, так как библиотека написана профессионалами, то в ней много inner и private классов и protected функций. Единственный правильный выход это переопределить метод ProcessUserAuthorization, таким образом, что наш InstagramClient : WebServerClient будет выглядет следующим образом:

Copy Source | Copy HTML
  1. public class InstagramClient : WebServerClient
  2. {
  3.     public UserInfo User;
  4.     private static readonly AuthorizationServerDescription Description = new AuthorizationServerDescription
  5.         {
  6.             AuthorizationEndpoint = new Uri("https://api.instagram.com/oauth/authorize/"),
  7.             ProtocolVersion = OAuth2.ProtocolVersion.V20,
  8.             TokenEndpoint = new Uri("https://api.instagram.com/oauth/access_token"),
  9.         };
  10.     /// <summary>
  11.     /// Initializes a new instance of the <see cref="InstagramClient"/> class.
  12.     /// </summary>
  13.     public InstagramClient()
  14.         : base(Description)
  15.     {
  16.         this.AuthorizationTracker = new TokenManager();
  17.     }
  18.     /// <summary>
  19.     /// Получение токена авторизации
  20.     /// </summary>
  21.     /// <param name="request"></param>
  22.     /// <returns></returns>
  23.     /// <remarks>http://instagr.am/developer/authentication/</remarks>
  24.     public new IAuthorizationState ProcessUserAuthorization(HttpRequestBase request = null)
  25.     {
  26.         if(HttpContext.Current == null || String.IsNullOrEmpty(HttpContext.Current.Request["code"]))
  27.         {
  28.             //redirect
  29.             return base.ProcessUserAuthorization(request);
  30.         }
  31.         Uri callback = MessagingUtilities.GetRequestUrlFromContext();
  32.         callback = new Uri(callback.Scheme+"://"+callback.Authority+callback.AbsolutePath);
  33.         //manual processing
  34. //'client_id=CLIENT-ID' \
  35. //-F 'client_secret=CLIENT-SECRET' \
  36. //-F 'grant_type=authorization_code' \
  37. //-F 'redirect_uri=YOUR-REDIRECT-URI' \
  38. //-F 'code=CODE' \https://api.instagram.com/oauth/access_token
  39.             var args = new Dictionary<string, string>
  40.                            {
  41.                                {"client_id", Instagram.APIKey.ClientId},
  42.                                {"client_secret", ConfigurationManager.AppSettings["instagramConsumerSecret"]},
  43.                                {"grant_type", "authorization_code"},
  44.                                {"redirect_uri", callback.OriginalString},
  45.                                {"code", HttpContext.Current.Request["code"]}
  46.                            };
  47.         string JSON = HttpClient.POST(Description.TokenEndpoint.OriginalString, args);
  48.             JObject obj = JObject.Parse(JSON);
  49.             if (obj["error_message"] != null)
  50.             {
  51.                 Debug.WriteLine(JSON);
  52.                 HttpContext.Current.Response.Redirect(String.Format("/?{0}", obj["error_message"].Value<string>()), true);
  53.             }
  54.             if(obj["user"]!=null)
  55.             {
  56.                 //(Model.Responses.UserResponse)Mapper.Map<Model.Responses.UserResponse>(GetJson(AuthInfo.User.Id))
  57.                 User = (UserInfo)Mapper.Map<UserInfo>(obj["user"].ToString());
  58.             }
  59.             return new AuthorizationState { AccessTokenIssueDateUtc = DateTime.Now, AccessToken = obj["access_token"].Value<string>() };
  60.     }
  61. }

Метки: ,   Категории:Code


Designing for Scale with Windows Azure Storage

Новый выпуск channel 9 рассказывающий про Azure Storage Tables. Это очень популярная тема, так как StorageTables скоро полностью вытеснят Azure SQL и уже поддерживают до 5000 операций в секунду из коробки. При этом, если почитать резюме ведущих разработчиков социальных сетей они гордятся какими-то 2000 операциями в секунду.
В самой телепередачи рассказано мало интересного, говорится банальное, что Ключевые поля должны быть селективными или селективными должны быть ключевые поля. Однако показана Tables Matrics Analitics , почему-то в описании не дана ссылка, которую может настроить себе любой, кто хочет отслеживать эффективность работы обращений в Azure Storage. Про аналитические возможности они заговорили, так как нет гарантий, что обращение к таблице будет обработано быстро. Иногда это может занимать секунды, поэтому пример показанный индусом про работы с миллиардами документов - в корне неправилен. Для большого объема запросов , также как и для блогов, часть документов должна подгружаться в оперативную память, для этого есть объектная модель, а обращения к таблицам или, что более привычно к SQL, должно делаться разово при инициализации. Соответственно при обращении к веб-роли, мы проверяем, что у нас в пямяти актуальные данные, что их не надо обновлять и быстренько возвращаем их пользователю. Если же пользователь делает какое-то действо, то нужно синхронизировать все инстансы через Azure Tables путем фонового обращения к таблицам. Именно фоновое, так как опять же нет никаких гарантий, что оно обработается быстро. Так например, чтобы не задерживать выдачу страницы из оперативной памяти, работа в Azure Tables в blogscloud реализована примерно таким кодом сощданием отдельного потока. Описание прокси класса SyncTimes публиковалось ранее.

Copy Source | Copy HTML
  1. Saved += (sender, args) =>
  2. {
  3.     Guid blogId = Blog.CurrentInstance.Id;
  4.     ThreadPool.QueueUserWorkItem(delegate
  5.     {
  6.         // because HttpContext is not available within this BG thread
  7.         // needed to determine the current blog instance,
  8.         // set override value here.
  9.         Blog.InstanceIdOverride = blogId;
  10.         foreach (SyncTime tableClass in SyncTimes<SyncTime>.CurrentInstance.GetAllInstances(Blog.CurrentInstance.Id.ToString()))
  11.         {
  12.             tableClass.UpdatePosts = DateTime.Now;
  13.             tableClass.UpdateDeletedPosts = DateTime.Now;
  14.             SyncTimes<SyncTime>.CurrentInstance.Update(tableClass, false);
  15.         }
  16.         SyncTimes<SyncTime>.CurrentInstance.Update(null);
  17.     });
  18. };

Метки: ,   Категории:Code


Azure Tables / Multi Role Counter

Пост содержит много кода и призван рассказать о реализации счетчика посещений в мульти-инстансовой мультипоточной среде, коей является любое веб приложение под Windows Azure. Впервые простая реализация счетчика была продемонстрирована в шоу channel9 и в целом он работал неправильно и решал лишь проблему мультипоточности при помощи Interlocked объекта.
Во-первых сразу определимся, что данные у нас по счетчику будут складироваться в Azure Table - по сути это следующее этап развития SQL баз данных, которые не требуют знания SQL, а позволяют описывать структуру объекта прямо в коде. Для работы с таблицами используется Entity Framework, для которого удобно использовать обертку SyncTimes примерно следующего вида:
Copy Source | Copy HTML
  1. /// <summary>
  2. /// Многопоточная работа с таблицами Азуре
  3. /// </summary>
  4. /// <typeparam name="T"></typeparam>
  5. public class SyncTimes<T> where T: TableServiceEntity,new()
  6. {
  7.     #region Azure Table's Row
  8.     public T _tc;
  9.     public T tc
  10.     {
  11.         get
  12.         {
  13.             if (_tc != null) return _tc;
  14.             _tc = Reload();
  15.             return _tc;
  16.         }
  17.         set { _tc = value; }
  18.     }
  19.     #endregion
  20.     public string roleid
  21.     {
  22.         get
  23.         {
  24.             return RoleEnvironment.IsAvailable ? RoleEnvironment.CurrentRoleInstance.Id : "0";
  25.         }
  26.     }
  27.     #region Работа через Таблицы
  28.     [ThreadStatic]
  29.     public static TableServiceContext _containerTable;
  30.     public TableServiceContext ContainerTable
  31.     {
  32.         get
  33.         {
  34.             if (_containerTable != null) return _containerTable;
  35.             lock (CurrentInstance)
  36.             {
  37.                 if (_containerTable != null) return _containerTable;
  38.                 CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
  39.                     "DefaultEndpointsProtocol=http;AccountName=imagecontainer;AccountKey=");
  40.                 CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
  41.                 tableClient.CreateTableIfNotExist(typeof(T).Name);
  42.                 // Retrieve a reference to a container  
  43.                 _containerTable = tableClient.GetDataServiceContext();
  44.                 _containerTable.IgnoreResourceNotFoundException = true;
  45.                 _containerTable.MergeOption = MergeOption.AppendOnly;
  46.             }
  47.             return _containerTable;
  48.         }
  49.         set { _containerTable = value; }
  50.     }
  51.     #endregion
  52.     public static SyncTimes<T> CurrentInstance
  53.     {
  54.         get
  55.         {
  56.             var table = HttpContext.Current.Items[typeof(T).Name] as SyncTimes<T>;
  57.             if (table != null) { return table; }
  58.             table = new SyncTimes<T>();
  59.             HttpContext.Current.Items[typeof(T).Name] = table;
  60.             return table;
  61.         }
  62.     }
  63.     /// <summary>
  64.     /// Создание запросного объекта
  65.     /// </summary>
  66.     /// <returns></returns>
  67.     public IQueryable<T> CreateQuery()
  68.     {
  69.         return ContainerTable.CreateQuery<T>(typeof(T).Name);
  70.     }
  71.     /// <summary>
  72.     /// загрузка-обновление таблицы
  73.     /// </summary>
  74.     /// <returns></returns>
  75.     private T Reload()
  76.     {
  77.         try
  78.         {
  79.             string blogid = Blog.CurrentInstance.Id.ToString();
  80.                 ReadOnlyCollection<EntityDescriptor> oEntities = ContainerTable.Entities;
  81.                 if (oEntities.Count >  0)
  82.                 {
  83.                     EntityDescriptor ed = oEntities.FirstOrDefault(
  84.                         p => p.Entity.GetType() == typeof (T) &&
  85.                              ((T) p.Entity).RowKey == roleid &&
  86.                              ((T) p.Entity).PartitionKey == blogid);
  87.                     if (ed != null)
  88.                     {
  89.                         ContainerTable.Detach(ed.Entity); //удалим из трекинга(!)
  90.                     }
  91.                 }
  92.                 T te = (from e in CreateQuery() where e.RowKey == roleid && e.PartitionKey == blogid select e).FirstOrDefault();
  93.                 if (te == null)
  94.                 {
  95.                     te = new T{PartitionKey = blogid,RowKey = roleid};
  96.                     ContainerTable.AddObject(typeof(T).Name, te);
  97.                     //ContainerTable.SaveChangesWithRetries();
  98.                 }
  99.                 return te;
  100.         }catch(Exception e1)
  101.         {
  102.             Trace.WriteLine(e1);
  103.         }
  104.         return null;
  105.     }
  106.     /// <summary>
  107.     /// Делаем обновление в БД.
  108.     /// </summary>
  109.     /// <remarks>передаем нул в качестве объекта, если хотим просто вызвать обновление</remarks>
  110.     /// <returns>TRUE if no error</returns>
  111.     public Boolean Update(object te, bool applychanges = true)
  112.     {
  113.             try
  114.             {
  115.                 if (te != null)
  116.                 {
  117.                         try
  118.                         {
  119.                             ContainerTable.UpdateObject(te);
  120.                         }
  121.                         catch(ArgumentException e0)//not tracking
  122.                         {
  123.                             ContainerTable.Detach(te);
  124.                             ContainerTable.AttachTo(typeof(T).Name, te, "*");
  125.                             ContainerTable.UpdateObject(te);
  126.                         }
  127.                         catch (DataServiceRequestException e1)//tracking by different uri
  128.                         {
  129.                             ContainerTable.Detach(te);
  130.                             ContainerTable.AttachTo(typeof(T).Name, te, "*");
  131.                             ContainerTable.UpdateObject(te);
  132.                         }
  133.                 }
  134.                 if (applychanges &&
  135.                     ContainerTable.Entities.Count(p => p.State != EntityStates.Unchanged && p.State!=EntityStates.Detached) >  0)
  136.                 {
  137.                     ContainerTable.SaveChangesWithRetries();
  138.                 }
  139.             }
  140.             catch (DataServiceRequestException ex)
  141.             {
  142.                 //значит объект не соответствует тому, что хранится в БД и нужно его обновить
  143.                 if (typeof(T) != typeof(HitsCounter))
  144.                 {
  145.                     Utils.Log(ex);
  146.                 }
  147.                 return false;
  148.             }
  149.         return true;
  150.     }
  151.     /// <summary>
  152.     /// Делаем обновление в БД.
  153.     /// </summary>
  154.     /// <returns></returns>
  155.     public T[] GetAllInstances(string blogid)
  156.     {
  157.         lock (ContainerTable)
  158.         {
  159.             try
  160.             {
  161.                 return (from e in CreateQuery() where e.PartitionKey == blogid && e.RowKey != roleid select e).ToArray();
  162.             }
  163.             catch (Exception e1)
  164.             {
  165.                 Trace.WriteLine(e1);
  166.             }
  167.         }
  168.         return null;
  169.     }
  170. }

Важно понимать, что [ThreadStatic] объект необъодимо занулять в начале хендлера каждого запроса, чтобы гарантировать, что внутри каждого потока у нас уникальный DataContext постоянный на протяжении всей обработки запроса. Теперь перейдем к реализации класса учета обращений. В моем случае мультипоточность учитывается классом ConcurrentDictionary и сама запись в Таблицу вызывается только при достижении 10 обработанных запросов в инстансе. Это не идеально, и имеет некоторую долю ошибок, однако реализация точного учета обращений в мульти-истансовой веб-роле привело бы к блокировки потока на 100мс, чего мы избегаем. Отмечу, что для разных инстонсов роли пишутся разные объекты в таблице, а при итоговом выводе, когда нужно получить сумму - они суммируются.
Copy Source | Copy HTML
  1. /// <summary>
  2. /// Summary description for TopPosts
  3. /// </summary>
  4. /// <remarks></remarks>
  5. [Extension("Counts and displays the number of viewers for a post", "3.0", "")]
  6. public class TopPosts
  7. {
  8.     /// <summary>
  9.     /// Initializes a new instance of the <see cref="TopPosts"/> class.
  10.     /// </summary>
  11.     /// <remarks></remarks>
  12.     static TopPosts()
  13.     {
  14.         Post.Serving += new EventHandler<ServingEventArgs>(OnPostServing);
  15.     }
  16.     /// <summary>
  17.     /// Called when [post serving].
  18.     /// </summary>
  19.     /// <param name="sender">The sender.</param>
  20.     /// <param name="e">The <see cref="BlogEngine.Core.ServingEventArgs"/> instance containing the event data.</param>
  21.     /// <remarks></remarks>
  22.     private static void OnPostServing(object sender, ServingEventArgs e)
  23.     {
  24.         NameValueCollection headers = HttpContext.Current.Request.Headers;
  25.         if (headers["X-moz"] == "prefetch")
  26.         {
  27.             return;
  28.         }
  29.         IPublishable ipub = (IPublishable)sender;
  30.         try
  31.         {
  32.             // Check For Single Post View, When viewing Specific Post, basically through post.aspx)
  33.             if (e.Location == ServingLocation.SinglePost)
  34.             {
  35.                 int viewCount;
  36.                 // Fetch out total views of current viewing post.
  37.                 viewCount = IncrementPostViewCount(ipub.Id.ToString());
  38.                 // Override the body of the post (temporary) to display total views
  39.                 if (Security.IsAuthenticated)
  40.                 {
  41.                     e.Body = String.Format(Resources.labels.totalViews + ": {0}<br/>", viewCount) + e.Body;
  42.                 }
  43.             }
  44.             else if (e.Location == ServingLocation.PostList && Security.IsAuthenticated)
  45.             {
  46.                 int viewCount = GetCountForPost(ipub.Id.ToString());
  47.                 // Override the body of the post (temporary) to display total views
  48.                 e.Body = String.Format(Resources.labels.totalViews + ": {0}<br/>", viewCount) + e.Body;
  49.             }
  50.         }
  51.         catch (Exception)
  52.         {
  53.         }
  54.     }
  55.     /// <summary>
  56.     /// Gets the popular posts.
  57.     /// </summary>
  58.     /// <param name="numberOfTopPosts">The number of top posts.</param>
  59.     /// <returns></returns>
  60.     /// <remarks></remarks>
  61.     public static List<KeyValuePair<string, int>> GetPopularPosts(int numberOfTopPosts)
  62.     {
  63.         List<KeyValuePair<string,int>> list = new List<KeyValuePair<string, int>>( 0);
  64.         IQueryable<HitsCounter> q = SyncTimes<HitsCounter>.CurrentInstance.CreateQuery();
  65.         HitsCounter[] posts =
  66.             (from e in q where e.PartitionKey == Blog.CurrentInstance.Id.ToString() select e).OrderByDescending(
  67.                 z => z.hits).Take(numberOfTopPosts).ToArray();
  68.         list.AddRange(posts.Select(hitsCounter => new KeyValuePair<string, int>(hitsCounter.RowKey, hitsCounter.hits)));
  69.         return list;
  70.     }
  71.     /// <summary>
  72.     /// Gets total view count for a certain post
  73.     /// </summary>
  74.     /// <param name="postId">the post id</param>
  75.     /// <returns>total live views count</returns>
  76.     /// <remarks></remarks>
  77.     public static int GetCountForPost(string postId)
  78.     {
  79.         return (from e in SyncTimes<HitsCounter>.CurrentInstance.CreateQuery() where e.RowKey == postId select e).
  80.             Sum(hitsCounter => hitsCounter.hits);
  81.     }
  82.     /// <summary>
  83.     /// Количество просомтров для обновления
  84.     /// </summary>
  85.     public static ConcurrentDictionary<string, int> oViews = new ConcurrentDictionary<string, int>();
  86.     /// <summary>
  87.     /// Increment view count of a post
  88.     /// </summary>
  89.     /// <param name="postId">Id of the post</param>
  90.     /// <returns>the post's view count</returns>
  91.     /// <remarks></remarks>
  92.     public static int IncrementPostViewCount(string postId)
  93.     {
  94.         int viewCount =  0;
  95.         try
  96.         {
  97.             string PartitionKey = RoleEnvironment.IsAvailable ? RoleEnvironment.CurrentRoleInstance.Id : "0";
  98.             PartitionKey += "_"+Blog.CurrentInstance.Id.ToString();
  99.             //если есть несохраненные изменения, то это для нас!
  100.             HitsCounter h = null;
  101.             IQueryable<HitsCounter> q = SyncTimes<HitsCounter>.CurrentInstance.CreateQuery();
  102.             HitsCounter[] hArray = (from e in q where e.RowKey == postId select e).ToArray();
  103.             foreach (HitsCounter hitsCounter in hArray)
  104.             {
  105.                 viewCount += hitsCounter.hits;
  106.                 if (hitsCounter.PartitionKey == PartitionKey)
  107.                 {
  108.                     h = hitsCounter;
  109.                 }
  110.             }
  111.             viewCount++;
  112.             bool success = false;
  113.             oViews.AddOrUpdate(postId, 1, (key, oldValue) => oldValue+1);
  114.             if (oViews[postId] > 10)
  115.             {
  116.                 if (h == null)
  117.                 {
  118.                     h = new HitsCounter(PartitionKey, postId)
  119.                             {
  120.                                 hits = oViews[postId]
  121.                             };
  122.                     SyncTimes<HitsCounter>.CurrentInstance.ContainerTable.AddObject("HitsCounter", h);
  123.                     success = SyncTimes<HitsCounter>.CurrentInstance.Update(null);
  124.                 }
  125.                 else
  126.                 {
  127.                     h.hits += oViews[postId];
  128.                     success = SyncTimes<HitsCounter>.CurrentInstance.Update(h);
  129.                     //успешное обновление
  130.                 }
  131.                 if (!success)
  132.                 {
  133.                     Utils.Log("Postid=" + postId + "; views=" + oViews[postId]);
  134.                 }
  135.                 else
  136.                 {
  137.                     int viewCount2;
  138.                     oViews.TryRemove(postId, out viewCount2);
  139.                 }
  140.             }
  141.         }
  142.         catch (DataServiceRequestException e1)
  143.         {
  144.             Utils.Log(e1);
  145.         }
  146.         catch(Exception e2)
  147.         {
  148.             Utils.Log(e2);
  149.         }
  150.         return viewCount;
  151.     }
  152. }

В ближайшие дни расскажу, почему обновление BlogEngine 2.6 содержит большой идеологический минус и о том, какие есть сложности в постронии блогохостинговой платформы.

Метки: , ,   Категории:Blogs | Code


Использование Azure CDN для динамических страниц

Как мы знаем, микрософт предлагает невероятные возможности по распространению контента во все точки мира, что заметно уменьшает время доступа к контенту. Это CDN. Как я писал ранее это сокращает трафик между континентами (бэкбонами). Но, чтобы включить CDN у простого сайта есть несколько особенностей, о которых напишу под катом, так как не достаточно просто включить CDN в панели управления и настроить CNAME домена.
1) Надо понимать, что если мы выставили context.Response.Cache.SetExpires(DateTime.Now.AddMinutes(10));//10min и сделали запрос через CDN, то результат этого запроса изменится не ранее чем через 10 минут.
2) В проекте создаем папку /cdn/ и настраиваем модуль rewrite для простой переброски всех запросов приходящих запросов следующим образом:
Copy Source | Copy HTML
  1. <system.webServer>
  2.   <rewrite>
  3.       <rules>
  4.         <rule name="0" stopProcessing="false">
  5.           <match url="^(?:cdn/)(.*)$"/>
  6.           <action type="Rewrite" url="{R:1}" appendQueryString="true"/>
  7.         </rule>
  8.     </rewrite>
  9.   </system.webServer>

3) Далее мы хотим, чтобы все наши ответы передавались в сжатом виде при помощи GZIP. Напомню, что GZIP отличается от deflate тем, что у последнего отсутствует избыточных заголовок. Итак, добавляем в проект модуль сжатия и настраиваем сжатие CompressionModule:
Copy Source | Copy HTML
  1. namespace Modules
  2. {
  3.     using System;
  4.     using System.IO;
  5.     using System.IO.Compression;
  6.     using System.Text;
  7.     using System.Text.RegularExpressions;
  8.     using System.Web;
  9.     using System.Web.UI;
  10.     using System.Net;
  11.     using System.Net.Sockets;
  12.     /// <summary>
  13.     /// Compresses the output using standard gzip/deflate.
  14.     /// </summary>
  15.     public sealed class CompressionModule : IHttpModule
  16.     {
  17.         #region Constants and Fields
  18.         /// <summary>
  19.         /// The deflate string.
  20.         /// </summary>
  21.         private const string Deflate = "deflate";
  22.         /// <summary>
  23.         /// The gzip string.
  24.         /// </summary>
  25.         private const string Gzip = "gzip";
  26.         #endregion
  27.         #region Public Methods
  28.         /// <summary>
  29.         /// Compresses the response stream using either deflate or gzip depending on the client.
  30.         /// </summary>
  31.         /// <param name="context">
  32.         /// The HTTP context to compress.
  33.         /// </param>
  34.         public static void CompressResponse(HttpContext context)
  35.         {
  36.             //Делаем так как у нас проксирование через CDN
  37.             context.Response.Cache.SetCacheability(HttpCacheability.Public);
  38.             context.Response.Cache.SetExpires(DateTime.Now.AddMinutes(10));//10min
  39.             System.Diagnostics.Debug.WriteLine("compressing --->" + context.Request.Path);
  40.             if (IsEncodingAccepted(Deflate))
  41.             {
  42.                 context.Response.Filter = new DeflateStream(context.Response.Filter, CompressionMode.Compress);
  43.                 WillCompressResponse = true;
  44.                 SetEncoding(Deflate);
  45.             }
  46.             else if (IsEncodingAccepted(Gzip))
  47.             {
  48.                 context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);
  49.                 WillCompressResponse = true;
  50.                 SetEncoding(Gzip);
  51.             }
  52.         }
  53.         #endregion
  54.         #region Private Methods
  55.         private static bool WillCompressResponse
  56.         {
  57.             get
  58.             {
  59.                 HttpContext context = HttpContext.Current;
  60.                 if (context == null) { return false; }
  61.                 return context.Items["will-compress-resource"] != null && (bool)context.Items["will-compress-resource"];
  62.             }
  63.             set
  64.             {
  65.                 HttpContext context = HttpContext.Current;
  66.                 if (context == null) { return; }
  67.                 context.Items["will-compress-resource"] = value;
  68.             }
  69.         }
  70.         #endregion
  71.         #region Implemented Interfaces
  72.         #region IHttpModule
  73.         /// <summary>
  74.         /// Disposes of the resources (other than memory) used by the module 
  75.         ///     that implements <see cref="T:System.Web.IHttpModule"></see>.
  76.         /// </summary>
  77.         void IHttpModule.Dispose()
  78.         {
  79.             // Nothing to dispose; 
  80.         }
  81.         /// <summary>
  82.         /// Initializes a module and prepares it to handle requests.
  83.         /// </summary>
  84.         /// <param name="context">
  85.         /// An <see cref="T:System.Web.HttpApplication"></see> 
  86.         ///     that provides access to the methods, properties, and events common to 
  87.         ///     all application objects within an ASP.NET application.
  88.         /// </param>
  89.         void IHttpModule.Init(HttpApplication context)
  90.         {
  91.             context.PreRequestHandlerExecute += ContextPostReleaseRequestState;
  92.             context.Error += new EventHandler(context_Error);
  93.         }
  94.         void context_Error(object sender, EventArgs e)
  95.         {
  96.             HttpContext context = ((HttpApplication)sender).Context;
  97.             Exception ex = context.Server.GetLastError();
  98.             // If this CompressionModule will be compressing the response and an unhandled exception
  99.             // has occurred, remove the WebResourceFilter as that will cause garbage characters to
  100.             // be sent to the browser instead of a yellow screen of death.
  101.             if (WillCompressResponse)
  102.             {
  103.                 context.Response.Filter = null;
  104.                 WillCompressResponse = false;
  105.             }
  106.         }
  107.         #endregion
  108.         #endregion
  109.         #region Methods
  110.         /// <summary>
  111.         /// Checks the request headers to see if the specified
  112.         ///     encoding is accepted by the client.
  113.         /// </summary>
  114.         /// <param name="encoding">
  115.         /// The encoding.
  116.         /// </param>
  117.         /// <returns>
  118.         /// The is encoding accepted.
  119.         /// </returns>
  120.         private static bool IsEncodingAccepted(string encoding)
  121.         {
  122.             var context = HttpContext.Current;
  123.             return context.Request.Headers["Accept-encoding"] != null &&
  124.                    context.Request.Headers["Accept-encoding"].Contains(encoding);
  125.         }
  126.         /// <summary>
  127.         /// Adds the specified encoding to the response headers.
  128.         /// </summary>
  129.         /// <param name="encoding">The encoding.</param>
  130.         private static void SetEncoding(string encoding)
  131.         {
  132.             HttpContext.Current.Response.AppendHeader("Content-encoding", encoding);
  133.         }
  134.         /// <summary>
  135.         /// Handles the BeginRequest event of the context control.
  136.         /// </summary>
  137.         /// <param name="sender">
  138.         /// The source of the event.
  139.         /// </param>
  140.         /// <param name="e">
  141.         /// The <see cref="System.EventArgs"/> instance containing the event data.
  142.         /// </param>
  143.         private static void ContextPostReleaseRequestState(object sender, EventArgs e)
  144.         {
  145.             var context = ((HttpApplication)sender).Context;
  146.             System.Diagnostics.Debug.WriteLine("precessing request --->" + context.Request.Path);
  147.             // only when page is requested 
  148.             if (context.CurrentHandler is Page &&
  149.                 context.Request["_TSM_HiddenField_"] == null &&
  150.                 context.Request["HTTP_X_MICROSOFTAJAX"] == null &&
  151.                 context.Request.HttpMethod == "GET")
  152.             {
  153.                 CompressResponse(context);
  154.             }
  155.         }
  156.         #endregion
  157.     }
  158. }

Copy Source | Copy HTML
  1.   <system.webServer>
  2.     <validation validateIntegratedModeConfiguration="false"/>
  3.     <staticContent>
  4.       <remove fileExtension=".js" />
  5.       <mimeMap fileExtension=".js" mimeType="text/javascript" />
  6.       <remove fileExtension=".css" />
  7.       <mimeMap fileExtension=".css" mimeType="text/css" />
  8.       <remove fileExtension=".ico"/>
  9.       <mimeMap fileExtension=".ico" mimeType="image/x-icon"/>
  10.       <remove fileExtension=".gif"/>
  11.       <mimeMap fileExtension=".gif" mimeType="image/gif"/>
  12.       <remove fileExtension=".woff"/>
  13.       <mimeMap fileExtension=".woff" mimeType="application/x-font-woff" />
  14.       <remove fileExtension=".svg"/>
  15.       <mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
  16.       <clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
  17.     </staticContent>
  18.     <httpCompression
  19.         directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files"
  20.         cacheControlHeader="max-age=86400"
  21.         noCompressionForHttp10="false"
  22.         noCompressionForProxies="false"
  23.         noCompressionForRange="false"
  24.         sendCacheHeaders="true" minFileSizeForComp="0">
  25.       <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" />
  26.       <dynamicTypes>
  27.         <add mimeType="text/*" enabled="true" />
  28.         <add mimeType="message/*" enabled="true" />
  29.         <add mimeType="application/x-javascript" enabled="true" />
  30.         <add mimeType="*/*" enabled="false" />
  31.       </dynamicTypes>
  32.       <staticTypes>
  33.         <add mimeType="text/*" enabled="true" />
  34.         <add mimeType="message/*" enabled="true" />
  35.         <add mimeType="application/javascript" enabled="true" />
  36.         <add mimeType="*/*" enabled="false" />
  37.       </staticTypes>
  38.     </httpCompression>
  39.     <urlCompression doStaticCompression="true" doDynamicCompression="true" />
  40.     <modules runAllManagedModulesForAllRequests="true">
  41.       <add name="CompressionModule" type="Modules.CompressionModule"/>
  42.     </modules>
  43. </system.webServer>

4) Сразу особенность в том, что в CompressionModule мы не включаем сжатие для context.Request["_TSM_HiddenField_"] == null && context.Request["HTTP_X_MICROSOFTAJAX"] - это Ajax скрипты для AjaxControlToolkit и прочего от микрософта.
5) И в завершение нам необходимо сделать перезапись поля action у aspForm, так как при олучении обращения через CDN оно будет сформировано как "/cdn/page.aspx" , что при выполнении PostBack будет ошибка. Перезаписать action можно с помощью RewriteFormHtmlTextWriter, который вставляется в masterpage как
Copy Source | Copy HTML
  1. protected override void Render(HtmlTextWriter writer)
  2. {
  3.     base.Render(new RewriteFormHtmlTextWriter(writer));
  4. }
Copy Source | Copy HTML
  1. namespace top30{
  2.     using System.IO;
  3.     using System.Web;
  4.     using System.Web.UI;
  5.     /// <summary>
  6.     /// The RewriteFormHtmlTextWriter class implements Form action tag rewriting for rewritten pages 
  7.     ///     on Mono.
  8.     /// </summary>
  9.     public class RewriteFormHtmlTextWriter : HtmlTextWriter
  10.     {
  11.         #region Constructors and Destructors
  12.         /// <summary>
  13.         /// Initializes a new instance of the <see cref="RewriteFormHtmlTextWriter"/> class.
  14.         /// </summary>
  15.         /// <param name="writer">
  16.         /// The writer.
  17.         /// </param>
  18.         public RewriteFormHtmlTextWriter(Html32TextWriter writer)
  19.             : base(writer)
  20.         {
  21.             this.InnerWriter = writer.InnerWriter;
  22.         }
  23.         /// <summary>
  24.         /// Initializes a new instance of the <see cref="RewriteFormHtmlTextWriter"/> class.
  25.         /// </summary>
  26.         /// <param name="writer">
  27.         /// The writer.
  28.         /// </param>
  29.         public RewriteFormHtmlTextWriter(TextWriter writer)
  30.             : base(writer)
  31.         {
  32.             this.InnerWriter = writer;
  33.         }
  34.         #endregion
  35.         #region Public Methods
  36.         /// <summary>
  37.         /// Writes the specified markup attribute and value to the output stream, and, if specified, writes the value encoded.
  38.         /// </summary>
  39.         /// <param name="name">
  40.         /// The markup attribute to write to the output stream.
  41.         /// </param>
  42.         /// <param name="value">
  43.         /// The value assigned to the attribute.
  44.         /// </param>
  45.         /// <param name="encode">
  46.         /// true to encode the attribute and its assigned value; otherwise, false.
  47.         /// </param>
  48.         public override void WriteAttribute(string name, string value, bool encode)
  49.         {
  50.                 if (name == "action")
  51.                 {
  52.                     if (HttpContext.Current.Items["ActionAlreadyWritten"] == null)
  53.                     {
  54.                         value = HttpContext.Current.Request.RawUrl.Replace("/cdn/","/");
  55.                         HttpContext.Current.Items["ActionAlreadyWritten"] = true;
  56.                     }
  57.                 }
  58.             base.WriteAttribute(name, value, encode);
  59.         }
  60.         #endregion
  61.     }
  62. }

Метки:   Категории:microsoft | Code


Gamification of Coding

Микрософт создало плагин для Visial Studio с системой ачивментов. Написал прогу - 1 очко, написал работающую прогу - 2 очка, написал прогу работающую на новых технологиях - 5 очков . С первой компиляцией получилось 67 поинтов.

Метки:   Категории:Code


csharp xml skip serialization for int field

При использовании XML сериализации удобно, чтобы в результат не попадали поля, которые содержат нулевые значения. Для стринговых и объектных полей все просто, достаточно в классе создать свойство, которое на get делает проверку значения объекта и если не нужно включать его в XML, то возвращаем null. В случае простого поля типа INT ( decimal) не удобно переходить от простого int к System.Nulable (int?). Но на помощь приходит весьма интересный патерн, о котором вероятно многие и не знали. Достаточно завести в классе свойство public bool ShouldSerialize{FieldName}() {return {FieldName}.HasValue;} , которое указывает, нужно ли сериализовать переменную {FieldName} или нет.

Метки:   Категории:Code


TechDays 2012 Netherlands

Презентация о нововведениях в asp4.5 и msvs2011 с последней конференции в Нидерландах.

Метки:   Категории:Code


Source Code Hightlight

На хабре наблюдается изменение в политике отображения вставок, поэтому изменил сервис подсветки кода, чтобы вырезались все пустые строчки при использовании HTML вывода. Получается компактно:
Copy Source | Copy HTML
public class Application
    {
        public Guid Id { get; set; }
        public String Name { get; set; }
        public Version CurrentVersion { get; set; }
        public String RootFolderPath { get; set; }
        public List<Update> Updates { get; set; }
    }
    public class Update
    {
        public String UpdateUrl { get; set; }
        public Version Version { get; set; }
        public bool IsInstalled { get; set; }
        public bool IsDownloaded { get; set; }
        public string UpdateLocalPath { get; set; }
    }
Напомню, что также непозволительно вставлять код вида ">0", который также автоматически заменяется на "> 0"

Метки:   Категории:bugs | Code


asp.net 4.5

Уже много хорошего было сказано про следующую версию asp.net , которая выйдет вместе с windows server 8.
Для тех кто пропустил. Вот видео про минимизацию и заметное ускорение открытия страниц. И вот еще общая концепция улучшений. Можно видеть, что большой упор сделан на асинхронные операции. В частности по получению ответов из WebResponse. Сейчас же приходится использовать много второстепенного кода для реализации асинхронного сичтывания. Ниже приведу фрагмент, который используется для асинхронной обработки ответов получаемых из ЖЖ. После нескольких ДДОС атак они стали параноидальными и включили в ответах chuncked, чтобы контролировать загруженность канала, поэтому синхронное считывание очень часто проваливается. Выход из этой проблемы реализуется следующим классом, который надеюсь станет неактуальным в 4.5:
Copy Source | Copy HTML
  1. #region Ассинхронное чтение из документации
  2.     public class RequestState
  3.     {
  4.         // This class stores the State of the request.
  5.         const int BUFFER_SIZE = 1024 * 256;
  6.         public StringBuilder requestData;
  7.         public byte[] BufferRead;
  8.         public HttpWebRequest request;
  9.         public HttpWebResponse response;
  10.         public Stream streamResponse;
  11.         public Encoding encoding;
  12.         public FileStream FileToWrite;
  13.         public RequestState()
  14.         {
  15.             BufferRead = new byte[BUFFER_SIZE];
  16.             requestData = new StringBuilder("");
  17.             request = null;
  18.             streamResponse = null;
  19.             encoding = Encoding.UTF8;
  20.             FileToWrite = null;
  21.         }
  22.     }
  23.  
  24.     class HttpWebRequest_BeginGetResponse
  25.     {
  26.         public ManualResetEvent allDone;
  27.         const int BUFFER_SIZE = 1024 * 256;
  28.         const int DefaultTimeout = 2 * 60 * 1000; // 2 minutes timeout
  29.  
  30.         // Abort the request if the timer fires.
  31.         private static void TimeoutCallback(object state, bool timedOut)
  32.         {
  33.             if (timedOut)
  34.             {
  35.                 HttpWebRequest request = state as HttpWebRequest;
  36.                 if (request != null)
  37.                 {
  38.                     request.Abort();
  39.                 }
  40.             }
  41.         }
  42.  
  43.         public RequestState ReadAsyncWebResp(HttpWebRequest myHttpWebRequest, string filepath)
  44.         {
  45.             // Create an instance of the RequestState and assign the previous myHttpWebRequest
  46.             // object to its request field.  
  47.             RequestState myRequestState = new RequestState();
  48.             allDone = new ManualResetEvent(false);
  49.             myRequestState.request = myHttpWebRequest;
  50.             //Open a file to write if needed
  51.             if (!String.IsNullOrEmpty(filepath))
  52.             {
  53.                 myRequestState.FileToWrite = File.Open(filepath, FileMode.OpenOrCreate);
  54.             }
  55.  
  56.             // Start the asynchronous request.
  57.             IAsyncResult result =
  58.               myHttpWebRequest.BeginGetResponse(RespCallback, myRequestState);
  59.  
  60.             // this line implements the timeout, if there is a timeout, the callback fires and the request becomes aborted
  61.             ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, TimeoutCallback, myHttpWebRequest, DefaultTimeout, true);
  62.  
  63.             // The response came in the allowed time. The work processing will happen in the 
  64.             // callback function.
  65.             allDone.WaitOne();
  66.  
  67.             // Release the HttpWebResponse resource.
  68.             myRequestState.response.Close();
  69.             if (myRequestState.FileToWrite != null)
  70.             {
  71.                 myRequestState.FileToWrite.Close();
  72.             }
  73.             allDone.Close();
  74.             return myRequestState;
  75.         }
  76.  
  77.         private void RespCallback(IAsyncResult asynchronousResult)
  78.         {
  79.             try
  80.             {
  81.                 // State of request is asynchronous.
  82.                 RequestState myRequestState = (RequestState)asynchronousResult.AsyncState;
  83.                 HttpWebRequest myHttpWebRequest = myRequestState.request;
  84.                 myRequestState.response = (HttpWebResponse)myHttpWebRequest.EndGetResponse(asynchronousResult);
  85.  
  86.                 // Read the response into a Stream object.
  87.                 Stream responseStream = myRequestState.response.GetResponseStream();
  88.                 myRequestState.streamResponse = responseStream;
  89.  
  90.                 // Begin the Reading of the contents of the HTML page and print it to the console.
  91.                 IAsyncResult asynchronousInputRead = responseStream.BeginRead(myRequestState.BufferRead,  0, BUFFER_SIZE, new AsyncCallback(ReadCallBack), myRequestState);
  92.                 return;
  93.             }
  94.             catch (WebException e)
  95.             {
  96.             }
  97.             allDone.Set();
  98.         }
  99.         private void ReadCallBack(IAsyncResult asyncResult)
  100.         {
  101.             try
  102.             {
  103.  
  104.                 RequestState myRequestState = (RequestState)asyncResult.AsyncState;
  105.                 Stream responseStream = myRequestState.streamResponse;
  106.                 int read = responseStream.EndRead(asyncResult);
  107.                 // Read the HTML page and then print it to the console.
  108.                 if (read >  0)
  109.                 {
  110.                     if (myRequestState.FileToWrite != null)
  111.                     {
  112.                         myRequestState.FileToWrite.Write(myRequestState.BufferRead,  0, read);
  113.                     }
  114.                     myRequestState.requestData.Append(myRequestState.encoding.GetString(myRequestState.BufferRead,  0, read));
  115.                     IAsyncResult asynchronousResult = responseStream.BeginRead(myRequestState.BufferRead,  0, BUFFER_SIZE, new AsyncCallback(ReadCallBack), myRequestState);
  116.                     return;
  117.                 }
  118.                 else
  119.                 {
  120.                     responseStream.Close();
  121.                 }
  122.  
  123.             }
  124.             catch (WebException e)
  125.             {
  126.             }
  127.             catch (IOException eio)
  128.             {
  129.             }
  130.             //пофиг на ошибки. что скачалось, то скачалось
  131.             allDone.Set();
  132.         }
  133.     }
  134.     #endregion

Метки:   Категории:Code


Power Shell from Windows Service

Некоторые особенности с организацией вызова PowerShell на WindowsServer 2008R2, для выполнения произвольного скрипта

1) Ставим PowerShell, через стандартную установку Features для WindowsServer, далее пишем примерно такую функцию
2) Важно, что ей можно передовать разовые команды, а можно имя файла, который нужно исполнить, для этого useScript ставим в true.
3) Вторая особенность неочевидна, так как PS выполняется в отдельном потоке под .Net 2.0 , а у вас .Net 4.0, то все токены прав потеряются, во избежание этого вы в приведенной выше функции указываем исполнять PS в том же потоке, что и текущий код (runspace.ThreadOptions = PSThreadOptions.UseCurrentThread;).
4) Следующей командой важно разрешить выполнение скриптов в отдельном файле. Так как по умолчанию это запрещено (scriptInvoker.Invoke("Set-ExecutionPolicy Unrestricted");).
5) И в завершение важно помнить, что PS выполняется с рабочей директорией System, которая отлична от вашей текущей, поэтому все относительные пути в файле скрипта могут не работать.


Метки:   Категории:microsoft | Code


Manual DNS Azure Solution with Power Shell

О проблеме настройки DNS для Windows Azure уже упоминалось, но оказалось все сложнее. Российский регистратор webnames не смог выставить DNS для доменов на нужные, ссылаясь на ошибки в SOA записях, даже техподдержка пока не помогла. Так что домены по 90р за штуку до добра не доведут. Поэтому я озадачился поднятием и настройкой своего пула DNS серверов и видимо с этим столкнется любой разработчик, который захочет работать с Windows Azure без использования CDN (Content Delivery Network).

Алгоритм настройки получился следующим:
1) Заказываем на windows хостинге, например Агаве, пару VPS серверов IIS Web за 500р в месяц, включаем на каждом из них DNS Server, на втором настраиваем репликацию первого.Заводим нужные dns-зоны. Записи A указывающую на IP адрес myserver.cloudapp.net и CNAME на сайт и приставкой www. Также настраиваем SOA, чтобы TTL был секунд 20, а не час, для более быстрого обновления.
2) На primary DNS сервере ставим PowerShell из стандартного пакета. Для этого открываем раздел Features в списке возможных установок на сервер. Далее пишем Windows Service который бы раз в 5 минут опрашивает ваши сайты на предмет не изменился ли у них IP адрес. Если изменился, то выполняем определенный PowerShell-скрипт, код которого приведен ниже.
3) Первый PS-скрипт вызывается, когда служба только запускается и ей нужно определить список всех доменных зон на сервере, делается это так:
Copy Source | Copy HTML
  1. Get-WmiObject -ComputerName localhost -Namespace 'root\MicrosoftDNS' -Class MicrosoftDNS_AType |Sort-Object -unique containername|Select containername,ipaddress

4) И второй PS-скрипт, когда обнаружены домены, для которых нужно поменять А записи, вызывается такой вот скрипт. Вызов выглядит как "./update-DNSAddress -forward localhost -reverse localhost". Где в файле dnsaddresslist.csv со списком нужных изменений IP адресов присутствует шапка "Name,Address" и далее по паре домен-айпи на каждой строчке разделенных запятой.

Это базовые знания, чтобы решить проблемы динамических адресов в Azure, но при желании могу добавить ваш домен на свои обновляемые DNS server'a.

bonus: Пример Csharp функции по запуску PowerShell скрипта, сохраненного на диск файлом, с параметром и получением результата работы скрипта.

Метки: ,   Категории:microsoft | Code


Отправка почты из Windows Azure

В Индусском Windows Azure есть много проблем. И одна из первых, с которой вы столкнетесь - невозможность отправить простое письмо во вне. Да это логичное ограничение настройками безопасности фаервола на всех системах, но так как это не одна машина, а облако - у вас нет возможности ее обойти. Даже через поиск находятся дурацкие решения, в которых предлагается либо 1) купить сторонний релай во вне и отправлять почту через него 2) стать подписчиком Exchange сервисов микрософта.

Но на самом деле, есть возможность отправки почты через Гугл, работающий через SSL и на неблокированном 587 порту. То есть делаем,
Copy Source | Copy HTML
  1. var client = new SmtpClient(props["SmtpServer"])
  2.                              {
  3.                                  DeliveryMethod = SmtpDeliveryMethod.Network,
  4.                                  Credentials = new NetworkCredential(props["SmtpLogin"], props["SmtpPwd"]),
  5.                                  Port = int.Parse(props["SmtpPort"]),
  6.                                  EnableSsl = true,
  7.                              };
  8.  
  9.             what = what + @"
    С Уважением, Почтовая служба " + props["HostAddress"];
  10.  
  11.             try
  12.             {
  13. #if !DEBUG
  14.                 client.Send(props["SmtpLogin"],
  15.                     to,
  16.                     "Subj",
  17.                     string.Format(what, pars));
  18. #endif
  19.             }
  20.             catch (SmtpFailedRecipientsException)
  21.             {
  22.  
  23.             }


и соответственно настройки
Copy Source | Copy HTML
  1. <add key="SmtpServer" value="smtp.gmail.com" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
  2. <add key="SmtpLogin" value="mail@gmail.com" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
  3. <add key="SmtpPwd" value="password" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
  4. <add key="SmtpPort" value="587" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />

Также, на всякий случай, добавляем в настройки разрешение на выполнение native кода -

PS: С июля этого года Azure вроде объявило о бесплатности всего входящего трафика.

Метки:   Категории:microsoft | Code


WCF Proxy tunnel

Ниже расскажу о реализации проброса WCF запросов от одного сервера через несколько промежуточных, так называемый wcf tunneling, вопрос о реализации которого уже поднимался на gotdotnet.ru. Там автор предлагал использовать Castle.DynamicProxy для динамической генерации прокси-классов для создания канала. Однако комментаторы отметили, что полученный функционал дублирует лишь стандартные возможности при прямом взаимодействии точка-точка.
Актуальность приобретается, когда добавляется набор промежуточных точек. При этом на клиенте идет перехват вызова произвольной удаленной функции FuncName на вызов абстрактной функции object _DataTransfer(HeaderAuthen auth, object ServiceLink, object FuncName, object[] param); Далее обращение транслируется через серию промежуточных машин и в итоге обращение передается на ServiceLink, где обрабатывается. Также возвращается результат. Грубо говоря примитивный функционал Biztalk своими руками по передачи soap сообщений в многосвязной сети. Кроме того мы без ведома пользователя накладываем на канал защиту через HeaderAuthen auth.
Отдельно выкладываю реализацию класс DynamicServiceProxy и ServiceChannelManagerInterceptor, подменяющие вызов, а также сам вызов в ServiceChannelManager. Соответственно все закладывается в библиотеку, которая должна быть на всех точках, а на туннельных-промежуточных точках еще и реализуется Контранкт, в котором важно описать передаточную функцию _DataTransfer, все функции на конечных точках, а также все классы и типы через KnownTypeContainer. Также важно включить сериализатор XmlSerializerFormat, так как по умолчанию в WCF используется DataContractSerializer, которых не позволяет передавать object. То есть вызовы проходить будут, а данные - нет. Кроме того у XmlSerializerFormat тоже есть особенность, кто нельзя передать некоторые стандартные типы, например, DataSet как параметр, так как в них используется нетипизированный ArrayList. Вот пожалуй и все, что нужно знать.

Метки:   Категории:Csharp | Code


Кто я?

Программист. Я слежу за блогосферой и знаю, как будет развиваться интернет. Когда у меня есть время я даже прилагаю для этого усилия. Подробнее

Последние комментарии

Не отображать

Topbot at FeedsBurner

Мои Твиты

Twitter октября 23, 17:57
8-й час в России http://dlvr.it/QpG99N https://twitter.com/f1ashr/status/1054793845546790912/photo/1

Twitter октября 18, 06:44
Яндекс атаковал Израиль http://dlvr.it/QnhhCy

Twitter октября 17, 16:07
Как разверифицироваться в Твиттере? http://dlvr.it/Qnddsc https://twitter.com/f1ashr/status/1052591848303120390/photo/1

Twitter октября 17, 16:07
На батуте в космос http://dlvr.it/QnddrT

Twitter октября 17, 16:07
Estonian Fitsme http://dlvr.it/Qnddn3

Twitter октября 17, 16:07
Россия будет воевать http://dlvr.it/Qnddq7

Twitter октября 17, 15:33
Про браузеры http://dlvr.it/QndT1N

Twitter октября 17, 14:54
Прыгающий Робо-жук http://dlvr.it/QndGjW https://twitter.com/f1ashr/status/1052573591181582336/photo/1

Twitter октября 17, 14:54
Google Plus выкатывает красивые имена http://dlvr.it/QndGj2

Twitter октября 17, 14:54
Немецкие совестливые журналисты http://dlvr.it/QndGjD

Twitter октября 17, 09:27
Победа свободы над здравым смыслом http://dlvr.it/QnbjB5 https://twitter.com/f1ashr/status/1052491176593354752/photo/1

Twitter октября 17, 06:41
программа instagramliker обновлена 2018г http://dlvr.it/Qnb2DZ

Twitter октября 16, 07:50
CloudSearch http://dlvr.it/QnTLzB

Twitter октября 16, 07:50
Сидеть вредно. http://dlvr.it/QnTLzD

Twitter октября 16, 07:50
Вконтакте 7 лет http://dlvr.it/QnTLzH

Twitter октября 16, 07:50
Рейтинг авторитетности блогов http://dlvr.it/QnTLz2

Twitter октября 16, 07:50
Tech news http://dlvr.it/QnTLz5

Twitter октября 16, 07:13
Совет многоквартирного дома http://dlvr.it/QnTCHQ https://twitter.com/f1ashr/status/1052095075918872576/photo/1

Twitter октября 16, 07:13
Windows 10 и Новый мировой порядок http://dlvr.it/QnTCF0 https://twitter.com/f1ashr/status/1052095069317001216/photo/1

Twitter октября 16, 07:13
Украинские солдаты до сих пор не поняли с кем воюют http://dlvr.it/QnTC5Z https://twitter.com/f1ashr/status/1052095061586980864/photo/1

Мой твиттер

Копирайт

Все мысли, высказанные в блоге, являются моим мнением и за это мнение меня никто не забанит! Кроме того, никто не имеет право копировать материалы блога без использования ctrl+C/V!

© Copyright 2008