יצירת פונקציות לטיפול במשימות ב-App Engine

בדף הזה מוסבר איך ליצור task handler ב-App Engine, שהוא קוד העובד שמטפל במשימה של App Engine. תור Cloud Tasks שולח בקשות HTTP ל-task handler. אחרי שהעיבוד מסתיים בהצלחה, ה-handler צריך לשלוח בחזרה לתור קוד סטטוס של HTTP בין 200 ל-299. כל ערך אחר מציין שהמשימה נכשלה והתור מנסה שוב לבצע את המשימה.

בקשות של תור המשימות של App Engine נשלחות מכתובת ה-IP‏ 0.1.0.2. אפשר גם לעיין בטווח כתובות ה-IP של בקשות שנשלחות לסביבת App Engine.

C#‎

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddLogging(builder => builder.AddDebug());
            services.AddRouting();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            var logger = loggerFactory.CreateLogger("testStackdriverLogging");

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // Configure error reporting service.
                app.UseExceptionHandler("/Home/Error");
            }

            var routeBuilder = new RouteBuilder(app);

            routeBuilder.MapPost("log_payload", context =>
            {
                // Log the request payload
                var reader = new StreamReader(context.Request.Body);
                var task = reader.ReadToEnd();

                logger.LogInformation($"Received task with payload: {task}");
                return context.Response.WriteAsync($"Printed task payload: {task}");
            });

            routeBuilder.MapGet("hello", context =>
            {
                // Basic index to verify app is serving
                return context.Response.WriteAsync("Hello, world!");
            });

            routeBuilder.MapGet("_ah/health", context =>
            {
                // Respond to GAE health-checks
                return context.Response.WriteAsync("OK");
            });

            routeBuilder.MapGet("/", context =>
            {
                return context.Response.WriteAsync("Hello, world!");
            });

            var routes = routeBuilder.Build();
            app.UseRouter(routes);
        }
    }

המשך


// Sample task_handler is an App Engine app demonstrating Cloud Tasks handling.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
)

func main() {
	// Allow confirmation the task handling service is running.
	http.HandleFunc("/", indexHandler)

	// Handle all tasks.
	http.HandleFunc("/task_handler", taskHandler)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("Defaulting to port %s", port)
	}

	log.Printf("Listening on port %s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
}

// indexHandler responds to requests with our greeting.
func indexHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	fmt.Fprint(w, "Hello, World!")
}

// taskHandler processes task requests.
func taskHandler(w http.ResponseWriter, r *http.Request) {
	taskName := r.Header.Get("X-Appengine-Taskname")
	if taskName == "" {
		// You may use the presence of the X-Appengine-Taskname header to validate
		// the request comes from Cloud Tasks.
		log.Println("Invalid Task: No X-Appengine-Taskname request header found")
		http.Error(w, "Bad Request - Invalid Task", http.StatusBadRequest)
		return
	}

	// Pull useful headers from Task request.
	queueName := r.Header.Get("X-Appengine-Queuename")

	// Extract the request body for further task details.
	body, err := io.ReadAll(r.Body)
	if err != nil {
		log.Printf("ReadAll: %v", err)
		http.Error(w, "Internal Error", http.StatusInternalServerError)
		return
	}

	// Log & output details of the task.
	output := fmt.Sprintf("Completed task: task queue(%s), task name(%s), payload(%s)",
		queueName,
		taskName,
		string(body),
	)
	log.Println(output)

	// Set a non-2xx status code to indicate a failure in task processing that should be retried.
	// For example, http.Error(w, "Internal Server Error: Task Processing", http.StatusInternalServerError)
	fmt.Fprintln(w, output)
}

Java

@WebServlet(
    name = "Tasks",
    description = "Create Cloud Task",
    urlPatterns = "/tasks/create"
)
public class TaskServlet extends HttpServlet {
  private static Logger log = Logger.getLogger(TaskServlet.class.getName());

  @Override
  public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    log.info("Received task request: " + req.getServletPath());
    String body = req.getReader()
        .lines()
        .reduce("", (accumulator, actual) -> accumulator + actual);

    if (!body.isEmpty()) {
      log.info("Request payload: " + body);
      String output = String.format("Received task with payload %s", body);
      resp.getOutputStream().write(output.getBytes());
      log.info("Sending response: " + output);
      resp.setStatus(HttpServletResponse.SC_OK);
    } else {
      log.warning("Null payload received in request to " + req.getServletPath());
    }
  }
}

Node.js

const express = require('express');

const app = express();
app.enable('trust proxy');

// Set the Content-Type of the Cloud Task to ensure compatibility
// By default, the Content-Type header of the Task request is set to "application/octet-stream"
// see https://cloud.google.com/tasks/docs/reference/rest/v2beta3/projects.locations.queues.tasks#AppEngineHttpRequest
app.use(express.text());

app.get('/', (req, res) => {
  // Basic index to verify app is serving
  res.send('Hello, World!').end();
});

app.post('/log_payload', (req, res) => {
  // Log the request payload
  console.log(`Received task with payload: ${req.body}`);
  res.send(`Printed task payload: ${req.body}`).end();
});

app.get('*', (req, res) => {
  res.send('OK').end();
});

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`App listening on port ${PORT}`);
  console.log('Press Ctrl+C to quit.');
});

PHP


require __DIR__ . '/vendor/autoload.php';

use Google\Cloud\Logging\LoggingClient;

// Create the logging client.
$logging = new LoggingClient();
// Create a PSR-3-compatible logger.
$logger = $logging->psrLogger('app', ['batchEnabled' => true]);

// Front-controller to route requests.
switch (@parse_url($_SERVER['REQUEST_URI'])['path']) {
    case '/':
        print "Hello, World!\n";
        break;
    case '/task_handler':
        // Taskname and Queuename are two of several useful Cloud Tasks headers available on the request.
        $taskName = $_SERVER['HTTP_X_APPENGINE_TASKNAME'] ?? '';
        $queueName = $_SERVER['HTTP_X_APPENGINE_QUEUENAME'] ?? '';

        try {
            handle_task(
                $queueName,
                $taskName,
                file_get_contents('php://input')
            );
        } catch (Exception $e) {
            http_response_code(400);
            exit($e->getMessage());
        }
        break;
    default:
        http_response_code(404);
        exit('Not Found');
}

/**
 * Process a Cloud Tasks HTTP Request.
 *
 * @param string $queueName provides the name of the queue which dispatched the task.
 * @param string $taskName provides the identifier of the task.
 * @param string $body The task details from the HTTP request.
 */
function handle_task($queueName, $taskName, $body = '')
{
    global $logger;

    if (empty($taskName)) {
        // You may use the presence of the X-Appengine-Taskname header to validate
        // the request comes from Cloud Tasks.
        $logger->warning('Invalid Task: No X-Appengine-Taskname request header found');
        throw new Exception('Bad Request - Invalid Task');
    }

    $output = sprintf('Completed task: task queue(%s), task name(%s), payload(%s)', $queueName, $taskName, $body);
    $logger->info($output);

    // Set a non-2xx status code to indicate a failure in task processing that should be retried.
    // For example, http_response_code(500) to indicate a server error.
    print $output;
}

Python

from flask import Flask, render_template, request

app = Flask(__name__)


@app.route("/example_task_handler", methods=["POST"])
def example_task_handler():
    """Log the request payload."""
    payload = request.get_data(as_text=True) or "(empty payload)"
    print(f"Received task with payload: {payload}")
    return render_template("index.html", payload=payload)

Ruby

require "sinatra"
require "json"

get "/" do
  # Basic index to verify app is serving
  "Hello World!"
end

post "/log_payload" do
  data = request.body.read
  # Log the request payload
  puts "Received task with payload: #{data}"
  "Printed task payload: #{data}"
end

חסימות זמניות

למשימות של App Engine יש הגדרות זמן קצוב לתפוגה ספציפיות, שמשתנות בהתאם לסוג ההתאמה לגודל של השירות שמריץ אותן.

לשירותי עובדים שפועלים בסביבה רגילה:

  • התאמה אוטומטית לעומס (automatic scaling): עיבוד המשימה חייב להסתיים תוך 10 דקות.
  • הגדלה ידנית ובסיסית: בקשות יכולות לפעול עד 24 שעות.

בשירותי Worker שפועלים בסביבת flex: כל הסוגים מוגבלים בזמן של 60 דקות.

אם המטפל לא יסיים את המשימה בזמן, התור יניח שהמשימה נכשלה וינסה לבצע אותה שוב.

קריאת כותרות של בקשות למשימות ב-App Engine

לבקשות שנשלחות ל-handler של App Engine מתור Cloud Tasks יש כותרות מיוחדות, שמכילות מידע ספציפי למשימה שה-handler עשוי להשתמש בו.

הכותרות האלה מוגדרות באופן פנימי. אם אחת מהכותרות האלה מופיעה בבקשה של משתמש חיצוני לאפליקציה, היא מוחלפת בכותרת הפנימית – למעט בקשות מאדמינים שמחוברים לאפליקציה, שמורשים להגדיר כותרות למטרות בדיקה.

בקשות למשימות ב-App Engine תמיד מכילות את הכותרות הבאות:

כותרת תיאור
X-AppEngine-QueueName שם התור.
X-AppEngine-TaskName השם ה "קצר" של המשימה, או, אם לא צוין שם כשנוצרה, מזהה ייחודי שנוצר על ידי המערכת. זהו הערך של my-task-id בשם המלא של המשימה. לדוגמה, task_name = projects/my-project-id/locations/my-location/queues/my-queue-id/tasks/my-task-id.
X-AppEngine-TaskRetryCount מספר הפעמים שהמשימה ניסתה שוב. בניסיון הראשון, הערך הוא 0. המספר הזה כולל ניסיונות שבהם המשימה נכשלה בגלל חוסר במופעים זמינים, ולא הגיעה לשלב הביצוע.
X-AppEngine-TaskExecutionCount מספר הפעמים שהמשימה בוצעה וקיבלה תגובה מהמטפל. מכיוון ש-Cloud Tasks מוחק את המשימה אחרי שמתקבלת תגובה מוצלחת, כל התגובות הקודמות של ה-handler הן כשלים. המספר הזה לא כולל כשלים שנובעים ממקרים שבהם לא היו מופעים זמינים. שימו לב: הערך של X-AppEngine-TaskExecutionCount יכול להיות שווה לערך של X-AppEngine-TaskRetryCount אם הוא מתעדכן לפני שמנסים להפעיל את הפונקציה.
X-AppEngine-TaskETA בתחילה, השעה המקורית שנקבעה למשימה, שצוינה בשניות מאז 1 בינואר 1970. שימו לב שהערך הזה מייצג את זמן השילוח הצפוי. בניסיונות חוזרים, הערך מתעדכן קרוב יותר לשעה הנוכחית ואפשר להשתמש בו כדי למדוד את זמן האחזור של המסירה. אין להשתמש בו לביטול כפילויות של משימות.

אם בקשת הטיפול מוצאת אחת מהכותרות שצוינו קודם, היא יכולה להניח שהבקשה היא בקשת Cloud Tasks.

בנוסף, בקשות מ-Cloud Tasks עשויות להכיל את הכותרות הבאות:

כותרת תיאור
X-AppEngine-TaskPreviousResponse קוד תגובת ה-HTTP מהניסיון הקודם.
X-AppEngine-TaskRetryReason הסיבה לניסיון החוזר של המשימה.
X-AppEngine-FailFast מציין שמשימה נכשלת באופן מיידי אם מופע קיים לא זמין.

ניתוב ליעד

במשימות של App Engine, גם התור וגם המטפל במשימות פועלים באותו Google Cloud פרויקט. התעבורה מוצפנת במהלך ההעברה ואף פעם לא יוצאת ממרכזי הנתונים של Google. אי אפשר להגדיר באופן מפורש את הפרוטוקול (למשל, HTTP או HTTPS). עם זאת, הבקשה ל-handler תופיע כאילו נעשה בה שימוש בפרוטוקול HTTP.

אפשר לשלוח משימות למטפלים מאובטחים במשימות, למטפלים לא מאובטחים במשימות ובזמני ריצה נתמכים, ל-URI שמוגבלים באמצעות login: admin. מכיוון שהמשימות לא מופעלות כמשתמש כלשהו, אי אפשר לשלוח אותן לכתובות URI שמוגבלות באמצעות login: required. גם בשליחת משימות אין מעקב אחרי הפניות אוטומטיות.

המאמרים הבאים