15

Windows Server 2003 で実行されている C# コンソール アプリを持っています。その目的は、Notifications というテーブルと "NotifyDateTime" というフィールドを読み取り、その時間に達したときに電子メールを送信することです。タスク スケジューラを介して 1 時間ごとに実行するようにスケジュールし、NotifyDateTime がその時間内にあるかどうかを確認してから、通知を送信します。

データベースに通知の日付/時刻があるため、このことを1時間ごとに再実行するよりも良い方法があるはずです。

テーブルからその日の通知を読み取り、期限が来たときに正確に発行する、サーバー上で実行したままにしておくことができる軽量のプロセス/コンソールアプリはありますか?

私はサービスを考えましたが、それはやり過ぎのようです。

4

8 に答える 8

27

私の提案は、Quartz.NETを使用する単純なアプリケーションを作成することです。

2 つのジョブを作成します。

  • まず、1 日に 1 回起動し、その日に計画されたデータベースからすべての待機中の通知時間を読み取り、それらに基づいていくつかのトリガーを作成します。
  • 次に、そのようなトリガーに登録され (最初のジョブによって準備されます)、通知が送信されます。

そのうえ、

このような目的のために Windows サービスを作成することを強くお勧めします。ただ、孤独なコンソール アプリケーションを常に実行させないようにするためです。同じアカウントでサーバーにアクセスできる人によって、誤って終了される可能性があります。さらに、サーバーを再起動する場合は、サービスを自動的に開始するように構成できますが、そのようなアプリケーションを手動で再度オンにすることを忘れないでください。

Web アプリケーションを使用している場合は、常にこのロジックをホストすることができます (例: IIS アプリケーション プール プロセス内)。そのようなプロセスはデフォルトで定期的に再起動されるため、アプリケーションが使用されていない真夜中にも動作するようにデフォルト設定を変更する必要があります。スケジュールされたタスクが終了しない限り。

更新(コード サンプル):

Manager クラス、ジョブのスケジューリングおよびスケジューリング解除のための内部ロジック。シングルトンとして実装された安全上の理由から:

internal class ScheduleManager
{
    private static readonly ScheduleManager _instance = new ScheduleManager();
    private readonly IScheduler _scheduler;

    private ScheduleManager()
    {
        var properties = new NameValueCollection();
        properties["quartz.scheduler.instanceName"] = "notifier";
        properties["quartz.threadPool.type"] = "Quartz.Simpl.SimpleThreadPool, Quartz";
        properties["quartz.threadPool.threadCount"] = "5";
        properties["quartz.threadPool.threadPriority"] = "Normal";

        var sf = new StdSchedulerFactory(properties);
        _scheduler = sf.GetScheduler();
        _scheduler.Start();
    }

    public static ScheduleManager Instance
    {
        get { return _instance; }
    }

    public void Schedule(IJobDetail job, ITrigger trigger)
    {
        _scheduler.ScheduleJob(job, trigger);
    }

    public void Unschedule(TriggerKey key)
    {
        _scheduler.UnscheduleJob(key);
    }
}

データベースから必要な情報を収集し、通知をスケジュールするための最初のジョブ(2 番目のジョブ):

internal class Setup : IJob
{
    public void Execute(IJobExecutionContext context)
    {
        try
        {                
            foreach (var kvp in DbMock.ScheduleMap)
            {
                var email = kvp.Value;
                var notify = new JobDetailImpl(email, "emailgroup", typeof(Notify))
                    {
                        JobDataMap = new JobDataMap {{"email", email}}
                    };
                var time = new DateTimeOffset(DateTime.Parse(kvp.Key).ToUniversalTime());
                var trigger = new SimpleTriggerImpl(email, "emailtriggergroup", time);
                ScheduleManager.Instance.Schedule(notify, trigger);
            }
            Console.WriteLine("{0}: all jobs scheduled for today", DateTime.Now);
        }
        catch (Exception e) { /* log error */ }           
    }
}

2 番目のジョブ、メール送信用:

internal class Notify: IJob
{
    public void Execute(IJobExecutionContext context)
    {
        try
        {
            var email = context.MergedJobDataMap.GetString("email");
            SendEmail(email);
            ScheduleManager.Instance.Unschedule(new TriggerKey(email));
        }
        catch (Exception e) { /* log error */ }
    }

    private void SendEmail(string email)
    {
        Console.WriteLine("{0}: sending email to {1}...", DateTime.Now, email);
    }
}

この特定の例の目的のためだけに、データベースのモックを作成します。

internal class DbMock
{
    public static IDictionary<string, string> ScheduleMap = 
        new Dictionary<string, string>
        {
            {"00:01", "foo@gmail.com"},
            {"00:02", "bar@yahoo.com"}
        };
}

アプリケーションの主なエントリ:

public class Program
{
    public static void Main()
    {
        FireStarter.Execute();
    }
}

public class FireStarter
{
    public static void Execute()
    {
        var setup = new JobDetailImpl("setup", "setupgroup", typeof(Setup));
        var midnight = new CronTriggerImpl("setuptrigger", "setuptriggergroup", 
                                           "setup", "setupgroup",
                                           DateTime.UtcNow, null, "0 0 0 * * ?");
        ScheduleManager.Instance.Schedule(setup, midnight);
    }
}

出力:

ここに画像の説明を入力

serviceを使用する場合は、このメイン ロジックをOnStartメソッドに配置するだけです (サービスの開始を待たずに、別のスレッドで実際のロジックを開始することをお勧めします。また、タイムアウトの可能性を回避することをお勧めします - この特定の例ではありません)。明らかに、しかし一般的に):

protected override void OnStart(string[] args)
{
    try
    {
        var thread = new Thread(x => WatchThread(new ThreadStart(FireStarter.Execute)));
        thread.Start();
    }
    catch (Exception e) { /* log error */ }            
}

その場合は、スレッドからのエラーをキャッチする WatchThread などのラッパーにロジックをカプセル化します。

private void WatchThread(object pointer)
{
    try
    {
        ((Delegate) pointer).DynamicInvoke();
    }
    catch (Exception e) { /* log error and stop service */ }
}
于 2013-09-22T21:39:08.600 に答える
4

Quartz.NET が適していると思われるスケジュールされたタスクとは対照的に、事前にスケジュールされたタスク (未定義の時間) は、一般的に扱いが面倒です。

さらに、中断/変更してはならないタスク (再試行、通知など) のファイア アンド フォーゲットと、積極的に管理する必要があるタスク (キャンペーンやコミュニケーションなど) を区別する必要があります。

ファイア アンド フォーゲット タイプのタスクには、メッセージ キューが適しています。送信先が信頼できない場合は、少なくともメッセージ固有の TTL を送信および再試行キュー。再試行レベルのキューをセットアップするためのコードへのリンクを含む説明を次に示します。

事前にスケジュールされたマネージ タスクでは、データベース キュー アプローチを使用する必要があります (スケジュールされたタスクのデータベース キューの設計に関する CodeProject の記事については、ここをクリックしてください)。これにより、所有権識別子を追跡している場合は、通知を更新、削除、または再スケジュールできます (たとえば、ユーザー ID を指定すると、ユーザーが死亡/購読解除などの通知を受信しなくなったときに、保留中のすべての通知を削除できます)。

スケジュールされた電子メール タスク (通信タスクを含む) には、より細かい制御 (有効期限、再試行、およびタイムアウト メカニズム) が必要です。ここで取るべき最善のアプローチは、電子メール タスクをそのステップ (有効期限、事前検証、テンプレート化、CSS のインライン化、リンクの絶対化、追跡オブジェクトの追加などのメール送信前のステップ) で処理できるステート マシンを構築することです。オープン トラッキング、クリック トラッキング用のリンクの短縮、事後検証、送信と再試行など)。

.NET SmtpClient が MIME 仕様に完全に準拠していないこと、および Amazon SES、Mandrill、Mailgun、Customer.io、Sendgrid などの SAAS 電子メール プロバイダーを使用する必要があることを認識していただければ幸いです。Mandrill または Mailgun を検討することをお勧めします。また、時間があれば、生の電子メールの送信を許可し、添付ファイル/カスタムヘッダー/DKIM 署名などを必ずしもサポートしていないプロバイダーの MIME メッセージを作成するために使用できるMimeKitを見てください。

これがあなたを正しい道に導くことを願っています。

編集

サービスを使用して、特定の間隔 (例: 15 秒または 1 分) でポーリングする必要があります。データベースの負荷は、一度に一定量の予定タスクをチェックアウトし、送信予定のメッセージの内部プールを維持することで、いくらか打ち消すことができます (タイムアウト メカニズムを配置します)。メッセージが返されない場合は、ポーリングをしばらく「スリープ」させます。データベース内の単一のテーブルに対してそのようなシステムを構築するのではなく、統合できる独立した電子メール スケジューリング システムを設計することをお勧めします。

于 2013-09-26T14:46:36.397 に答える
1

スケジュールされたタスクは、(毎時、毎日などではなく) 特定の時間に 1 回だけ実行されるようにスケジュールできるため、データベース内の特定のフィールドが変更されたときにスケジュールされたタスクを作成するという 1 つのオプションがあります。

使用するデータベースについては言及していませんが、SQL などでトリガーの概念をサポートしているデータベースもあります

于 2013-09-22T04:32:38.980 に答える
1

私は約3年前に同じ問題に取り組んできました。プロセスが十分に良くなる前に、プロセスを数回変更しました。その理由を説明します。

  1. 最初の実装では、IIS Web サイトを呼び出す Web ホスティングの特別なデーモンを使用していました。Web サイトは発信者 IP を確認し、データベースを確認してメールを送信しました。これは、ある日まで機能していましたが、ユーザーから非常に汚いメールを大量に受け取り、メールボックスを完全にスパムしました。電子メールをデータベースに保持し、SMTP 電子メールから送信することの欠点は、何もないことですこれにより、DB から SMTP へのトランザクションが保証されます。メールが正常に送信されたかどうかはわかりません。電子メールの送信は、成功、失敗、誤検知、または誤検知の可能性があります (SMTP クライアントは、電子メールが送信されなかったが、送信されたと通知します)。SMTPサーバーに問題があり、サーバーはfalse(メールは送信されません)を返しましたが、メールは送信されました。デーモンは、ダーティ メールが表示される前に、1 時間ごとにメールを再送信していました。

  2. 2 番目の実装: スパムを防止するために、アルゴリズムを変更し、送信に失敗した場合でもメールが送信されたと見なされるようにしました (メール通知はそれほど重要ではありませんでした)。私の最初のアドバイスは次のとおりです。

  3. 数か月後、サーバーにいくつかの変更が加えられ、デーモンがうまく機能しなくなりました。stackoverflow からアイデアを得ました: .NET タイマーを Web アプリケーション ドメインにバインドします。メモリ リークが原因で IIS が時々アプリケーションを再起動する可能性があり、再起動がタイマー ティックよりも頻繁に行われるとタイマーが起動しないように思われるため、これは良い考えではありませんでした。

  4. 最後の実装。Windows スケジューラは、1 時間ごとに、ローカル Web サイトを読み取る Python バッチを起動します。これにより、ASP.NET コードが起動されます。利点は、タイム ウィンドウ スケジューラがローカル バッチと Web サイトを確実に呼び出すことです。IIS はハングしません。再起動機能があります。タイマー サイトは私のウェブサイトの一部であり、まだ 1 つのプロジェクトです。(代わりにコンソール アプリを使用できます)。シンプルイズベスト。それはうまくいきます!

于 2013-09-26T18:45:37.673 に答える
1

電子メールを事前に送信する必要がある時期がわかっている場合は、適切なタイムアウトでイベント ハンドルを待機することをお勧めします。真夜中にテーブルを見て、次の電子メールを送信する必要があるときにタイムアウトが期限切れになるように設定されたイベント ハンドルを待ちます。電子メールを送信した後、次に送信する必要のあるメールに基づいてタイムアウトを設定して、もう一度待機します。

また、説明に基づいて、これはおそらくサービスとして実装する必要がありますが、必須ではありません。

于 2013-09-25T20:55:29.257 に答える