scheduler

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 5, 2023 License: MIT Imports: 11 Imported by: 0

README

go-scheduler!

your personal scheduling assistant

Overview

The go-scheduler library is a highly customizable tool that empowers developers to schedule, persist, and manage job schedules effortlessly.

With go-scheduler, developers can define functions and schedule them to execute at specified times.

The go-scheduler library is:

  • Customizable -> The library provides numerous configuration options to better align with your requirements.
  • Persistent -> Job schedules are persistent, ensuring that even if your application encounters issues, the schedule information remains intact.
  • Friendly -> go-scheduler prioritizes a user-friendly API for managing jobs, allowing developers to create clean, scalable, and readable code!

Ex.:

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

// create your job functions
func myJobFunction(job *scheduler.Job) (err error) {
	fmt.Println("JOB TRIGGERED!!!!")

  // your business logic goes here...
	return
}

func main() {
  // init the library
  scheduler.Init(/* you configuration goes here */)

  // define the job
  scheduler.Define("myJob", myJobFunction)

  // then you can schedule in a specific time duration...
  oneHour := time.Hour
  scheduler.In(oneHour).Do("myJob")

  // or in a specific date...
  nextWeek := time.Now().Add(time.Hour*24*7)
  scheduler.On(nextWeek).Do("myJob")

  // or you can even schedule recurrent jobs!
  scheduler.Every("monday at 16:07").Do("myJob")
}

Setup

To download go-scheduler and add it to your project, just run:

$ go get github.com/delivery-much/go-scheduler

And you're good to Go!

Configuring the library

When initiating the go-scheduler library, developers must provide a Config struct with the library necessary configuration.

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

func main() {
  // init the library
  scheduler.Init(scheduler.Config{
    // your configuration
  })
}

The Config struct has the following values:

  • DB -> Represents a user created struct that implements the JobDatabase interface. Users should provide this value when they want to have full controll over the actions that the library will execute in its database, or if they whant to use other database other than mongoDB. If no user created DB is specified, the MongoDB value should be provided. (See Start the library providing your own Database section for more)

  • MongoDB -> Represents the configuration values that the library need to start a job DB on a mongoDB connection. This configuration uses the original mongoDB driver to do so. When providing this configuration, the library will have access to the user's mongoDB connection, since it will be responsible for managing jobs. If this value is not specified, the DB value should be provided. (See Start the library providing a mongo connection section for more)

  • Logger -> Represents a user created struct that implements the Logger interface. This logger will be used by the library to log information whenever necessary. If no logger is specified, the library will log nothing. (See Providing a Logger to the library section for more)

  • ProcessingRate -> Represents the rate that the library will process jobs. If the value is not specified, the default rate is 1 minute.

  • Location -> Represents the location that the library should use when generating time values. If the value is not specified, the default location is UTC. (See Configuring the library location section for more)

  • DeleteOnDone -> Defines if, when a job is done, the job should be deleted from the database. If the value is not specified, the default value is false.

  • DeleteOnDone -> Defines if, when a job is canceled, the job should be deleted from the database. If the value is not specified, the default value is false.

The only really required values are either the MongoDB or the DB value, the other values are optional. However, is highly recommended to provide the Logger value, so you can keep track of the job process.

Start the library providing a mongo connection

To make developers lives easier, go-scheduler library allows them to provide a mongoDB connection directly when starting the library.

In this case, go-scheduler will be responsible for accessing and manipulating the job database, and your only worry will be to define and schedule your jobs.

The configuration should look something like this:

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
  "go.mongodb.org/mongo-driver/mongo"
)

func main() {
  var myConnection *mongo.Client
  // ... connect to mongoDB

  // init the library
  scheduler.Init(scheduler.Config{
    MongoDB: &scheduler.MongoJobDBConfig{
	    Conn: myConnection,
	    DbName: "MyDB",
    	CollName: "MyCollection",
    },
  })
}
  • Conn -> Its a pointer to your mongoDB connection! go-scheduler expects a Client struct from the default mongo driver. If no value is specified, the library initiation will fail.

  • DbName -> Its the database name that the library should use to save jobs. If no value is specified, the default is "go-scheduler".

  • CollName -> Its the collection name that the library should use to save jobs. If no value is specified, the default is "scheduler-jobs".

⚠️ DISCLAIMER: go-scheduler will NOT access any other database or collection than the ones that the user specified.

Start the library providing your own Database

In case you don't want to give go-scheduler access to your database for safety reasons, or you don't want to work with mongo, there's no problem!! You can also start the library with your own custom database.

In this case, the developer has total control over the database access, and is responsible for saving, reading and deleting jobs.

The database you provide must be a struct that implements the JobDatabase interface:

type JobDatabase interface {
	InitJobDB() error
	ListExpiredSchedules() ([]*Job, error)
	SaveJob(j Job) error
	List(f Finder) ([]*Job, error)
	DeleteJob(j Job) error
}

Where:

  • InitJobDB -> Its a function that will be called at the beginning of the library instantiation. It should start the job database and make it ready to read and write jobs. In this method you can create your database indexes, run your migrations, or anything you want to do so your database is ready to manage jobs.

  • ListExpiredSchedules -> Its a function that will be called when the library requests the jobs that should run. It should search the database for any job schedules that are expired, and return a list of pointers to those jobs.

  • SaveJob -> Its a function that will be called when the library needs to save a job on the database. It should receive a job struct, and "upsert" it in the database. (If it's a new job, should insert a new job, if its an existent job, should update the existent job).

  • DeleteJob -> Its a function that will be called when the library needs to delete a job. It should receive a job struct, and remove it completely from the database.

  • List -> Its a function that will be called when the developer wants to list jobs outside of the normal schedule flow. (See Listing jobs manually section for more).

    This method receives a Finder struct, and should use the values inside the finder to list jobs in the database. Currently, the Finder allows developers to find jobs by name, status or by the extra data that was provided when the job was scheduled (See Schedule your job section for more).

    type Finder struct {
        Status string
        Name   string
        Data   map[string]any
    }
    

    The developer should parse the Finder struct correctly into a filter that better suits the database used, and return the jobs accordingly.

It's very important to note that, to ensure the correct execution of the library, it's imperative that the Job struct is saved and read correctly from the database. The job struct goes as such:

type Job struct {
	ID                string
	ScheduleType      ScheduleType
	Status            ScheduleStatus
	NextRunAt         time.Time
	LastRunAt         *time.Time
	ScheduleString    string
	ScheduleLimitDate *time.Time
	Name              string
	Data              map[string]any
}

Every job field available should be mapped and saved correctly in the database.

When you have your database implementation ready, you can provide it when starting the library!

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

type myDB struct { }

func (db *myDB) InitJobDB() error {
  // ... your implementation
  return nil
}

func (db *myDB) ListExpiredSchedules() ([]*Job, error) {
  // ... your implementation
  return []*Job{}, nil
}

func (db *myDB) SaveJob(j Job) error {
  // ... your implementation
  return nil
}

func (db *myDB)	DeleteJob(j Job) error {
  // ... your implementation
  return nil
}

func (db *myDB)	List(f scheduler.Finder) ([]*Job, error) {
  // ... your implementation
  return []*Job{}, nil
}

func main() {
  db := &myDB{}

  // init the library with your custom database
  scheduler.Init(scheduler.Config{
    DB: db,
  })
}

Providing a Logger to the library

When dealing with jobs that are running in the background of your application, it's extremely important to keep track of failures during the job execution.

For that reason, the go-scheduler library allows developers to provide a logger when the library starts.

The logger should be a struct that implements the Logger interface:

type Logger interface {
	Error(message string)
	Errorf(format string, a ...any)
}

The Logger interface is fairly simple, it has two methods:

  • Error -> Which should log an error message

  • Errorf -> Which should format the message and log it on error level

Developers should implement the Logger interface as they please, and provide it when the library is initiated:

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

type myLogger struct { }

func (logger *myLogger) Error(message string) {
  // your logging goes here
}

func (logger *myLogger) Errorf(format string, a ...any)  {
  // your logging goes here
}

func main() {
  logger := &myLogger{}

  // init the library with your custom logger
  scheduler.Init(scheduler.Config{
    Logger: logger,
  })
}

Whenever a job execution fails, the library will use this logger struct to log an informative error message.

If no logger is specified, the library will not log any errors.

Configuring the library location

When dealing with dates, it's important to have control over the timezone which the dates are in.

Very often the go-scheduler library will instantiate dates, and it's important for the library to know which timezone the developer wants to use.

Because of that, developers can provide the Location that the library should use when initiating the library:

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

func main() {
  // init the library
  scheduler.Init(scheduler.Config{
    Location: "America/Sao_Paulo",
  })
}

The Location its a string field in the library configuration that developers can provide to specify which timezone should be used when instantiating dates.

If no value is specified, the timezone will be set to UTC.

⚠️ DISCLAIMER: It's crucial to note that the provided string must be a valid location, and the machine running the library should have the specified location available.

If the location is invalid or unavailable, the Init function will return an error.

Scheduling jobs

After you have Configured the library, you are all set to define and schedule jobs!

The following sections are a step-by-step guide for defining and handling jobs.

1. Create your job function

The first step is to create the logic flow that should be executed when the job triggers.

For that, developers should create job functions!

The job functions are functions that should implement the JobFunc contract:

type JobFunc func(*Job) (err error)

They should receive a pointer to a job, do all the necessary logic, and then return an error if anything went wrong.

  • If the job function returns an error, the library will set the job status as FAILED.

  • If the job function returns no error, the library will set the job status as DONE.

Since the job function receives a pointer to the Job struct, developers can also alter the job struct itself if necessary, and the library will save it on the database with those changes.

⚠️ DISCLAIMER: Given the fact above, it's not recommended for the developer to alter key values of the job (like the job ScheduleType or Status for instance), since it might break the library flow.

2. Define the job

After you have Created your job function you can define your job.

Defining jobs is easy, all you need to do is call the Define function, passing the job name and the function that should be executed when a job with that name is triggered.

It's very important to note that, different from job schedules, job definitions are not persisted, and should be defined everytime that your application is executed.

Ex.:

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

func MyJobFunc(j *Job) (err error) {
  // your business logic goes here
  return nil
}

func main() {
  // init the library
  scheduler.Init(scheduler.Config{
    // your configuration
  })

  scheduler.Define("myJobName", MyJobFunc)
}

After that, everytime that a job with the name "myJobName" is triggered, the MyJobFunc will be called!

3. Schedule your job

After you have succesfully Defined your job, you can schedule it!

To schedule jobs, developers should use one of the schedule functions available, and then subsequently call the Do function so that the job can be saved correctly in the database.

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

// create your job functions
func myJobFunction(job *scheduler.Job) (err error) {
  // your business logic goes here...
	return
}

func main() {
  // init the library
  scheduler.Init(scheduler.Config{
    // your configuration
  })

  // define the job
  scheduler.Define("myJobName", MyJobFunc)

  myExtraData := map[string]any{
    "myIntField": 42,
    "myDateField": time.Now(),
  }
  oneHour := time.Hour

  err := scheduler.In(oneHour).Do("myJob", myExtraData)
  if err != nil {
    fmt.Error("Failed to schedule job!")
  }
}

The Do function receives the job name that was previously defined, and an optional map with any extra data that the user wants to save with the job.

There are three main function that the developer can use to schedule jobs using the go-scheduler library:

In

The In function will schedule a job to be executed once in the provided duration:

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

// create your job functions
func myJobFunction(job *scheduler.Job) (err error) {
  // your business logic goes here...
	return
}

func main() {
  // init the library
  scheduler.Init(scheduler.Config{
    // your configuration
  })

  // define the job
  scheduler.Define("myJobName", MyJobFunc)

  // then you can schedule in a specific time duration...
  oneHour := time.Hour
  scheduler.In(oneHour).Do("myJobName")
}
On

The On function will schedule a job to be executed once in the provided date:

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

// create your job functions
func myJobFunction(job *scheduler.Job) (err error) {
  // your business logic goes here...
	return
}

func main() {
  // init the library
  scheduler.Init(scheduler.Config{
    // your configuration
  })

  // define the job
  scheduler.Define("myJobName", MyJobFunc)

  nextWeek := time.Now().Add(time.Hour*24*7)
  scheduler.On(nextWeek).Do("myJobName")
}

⚠️ DISCLAIMER: It's crucial to note that the provided date should be in the same timezone as it was configured in the library instantiation. (See Configuring the library location section for more)

Every

The Every function will schedule a job to be executed repeatedly, given a duration string.

This duration string accepts four formats:

  • A time interval string (Ex.: "minute", "2 months", "6 years");

  • A time string in HH:MM format (Ex.: "11:27");

    In this scenario, the job will be scheduled to run every day at the specified hour. If the specified hour has already passed on the day the job is being defined, the job will be scheduled for the next day.

  • A weekday string (Ex.: "monday", "friday");

    In this scenario, the job will be scheduled to run every week on the specified weekday, beginning at the first minute of the day (00:01). If the specified weekday has already passed in the current week during the job definition, the job will be scheduled for the next week. If the job is being scheduled on the specified weekday, it will be scheduled for the next week.

  • A weekday and time string (Ex.: "monday at 12:00", "friday at 15:08").

    In this scenario, the job will be scheduled to run every week on the specified weekday, beginning at the specified hour. If the specified weekday and/or hour has already passed in the present time during the job definition, the job will be scheduled for the next week.

Everytime that the job runs successfully, it will be re-scheduled as a PENDING job.

If the job fails, the job status will be set as FAILED and will not be re-scheduled.

Also, developers can use the Until function to provide a limit date to the recurrent job. If the job has a limit, the job will be set as DONE if it executes correctly and the limit date arrived.

Ex.:

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

// create your job functions
func myJobFunction(job *scheduler.Job) (err error) {
  // your business logic goes here...
	return
}

func main() {
  // init the library
  scheduler.Init(scheduler.Config{
    // your configuration
  })

  // define the job
  scheduler.Define("myJobName", MyJobFunc)

  // schedule a recurrent job with no limit
  scheduler.Every("monday at 13:00").Do("myJobName") // this job will run forever, unless it fails or its manually canceled or deleted

  // schedule a recurrent job with a limit
  nextWeek := time.Now().Add(time.Hour*24*7)
  scheduler.Every("hour").Until(nextWeek).Do("myJobName") // this job will run until next week.
}

Manually handling jobs

The go-scheduler library takes care of the majority of job handling for you, but there may be instances where developers want to manage specific jobs outside the regular job flow.

If you wish to cancel jobs, delete jobs, or manually change their status, we've got you covered!

Listing jobs manually

Developers can list jobs manually using the List function!

Ex.:

import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

func main() {
  jobs, err := scheduler.List(scheduler.Finder{
    Name:   "MyJobName",
    Status: "PENDING",
    Data: map[string]any{
      "my_data_field": "myDataValue",
    },
  })
}

The List function receives a Finder struct. This struct is used to pass parameters to the listing action.

Currently, the Finder struct allows developers to find jobs by name, status or by the extra data that was provided when the job was scheduled (See Schedule your job section for more).

type Finder struct {
  Status string
  Name   string
  Data   map[string]any
}

The List method then returns a list of pointer to jobs that where found given the filter, and an error if anything went wrong.

Handling jobs

After you have your jobs that you want to handle, the Job struct provides a series of function that can help you manage it.

  • Done -> Will set the job status as DONE and save it on the database;

    If the library was configured as DeleteOnDone, the job will be deleted instead.

  • Cancel -> Will set the job status as CANCELED and save it on the database;

    If the library was configured as DeleteOnCancel, the job will be deleted instead.

  • Fail -> Will set the job status as FAILED and save it on the database;

  • Delete -> Will delete the job from the database;

Ex.:


import (
  "time"
  "fmt"

  "github.com/delivery-much/go-scheduler"
)

func main() {
  // First you list jobs with the necessary filter...
  jobs, err := scheduler.List(scheduler.Finder{
    Name:   "MyJobName",
    Status: "PENDING",
    Data: map[string]any{
      "my_data_field": "myDataValue",
    },
  })
  if err != nil {
    fmt.Error("Failed to manage job!")
  }

  for _, j := range jobs {
    // ... then you can set it as done ...
    err = j.Done()
    if err != nil {
      fmt.Error("Failed to manage job!")
    }

    // ... or set it as failed ...
    err = j.Fail()
    if err != nil {
      fmt.Error("Failed to manage job!")
    }

    // ... or cancel it ...
    err = j.Cancel()
    if err != nil {
      fmt.Error("Failed to manage job!")
    }

    // ... or event delete it!
    err = j.Delete()
    if err != nil {
      fmt.Error("Failed to manage job!")
    }
  }
}

Documentation

Index

Constants

View Source
const (
	DONE     = ScheduleStatus("DONE")
	FAILED   = ScheduleStatus("FAILED")
	PENDING  = ScheduleStatus("PENDING")
	CANCELED = ScheduleStatus("CANCELED")
)
View Source
const (
	SIMPLE    = ScheduleType("SIMPLE")
	RECURRENT = ScheduleType("RECURRENT")
)

Variables

This section is empty.

Functions

func Define

func Define(jobName string, fn JobFunc)

Define inserts a new job definition given the job name and the function to be called when that job is triggered.

If the jobName was already previously defined, the previous function will be overridden

func Every

func Every(schedule string) *recurrentScheduleDefinition

Every schedules a RECURRENT job to run repeatedly, given the schedule string.

The schedule string expects the following formats:

- A time interval string (Ex.: "1 minute", "2 months", "6 years")

- A time string in HH:MM format (Ex.: "11:27")

- A weekday string (Ex.: "monday", "friday")

- A weekday and time string (Ex.: "monday at 12:00", "friday at 15:08")

func In

func In(d time.Duration) *simpleScheduleDefinition

In creates a new definition of a SIMPLE and PENDING job to be run once in the provided duration.

This function does not save the schedule in the database yet, the function Do must be called subsequently so that the job can be defined and saved.

func Init

func Init(c Config) (err error)

Init initis the scheduler library

func On

func On(t time.Time) *simpleScheduleDefinition

On creates a new definition of a SIMPLE and PENDING job to be run once in the provided date time.

This function does not save the schedule in the database yet, the function Do must be called subsequently so that the job can be validated and saved.

IMPORTANT: Please note that, for the library flow to function correctly, the provided time value (t) should be in the same timezone as configured during the library instantiation.

Types

type Config

type Config struct {
	// DB represents a user created struct that implements the JobDatabase interface.
	//
	// Users should provide this value when they want to have full controll over the
	// actions that the library will execute in its database.
	//
	// If no user created DB is specified, the MongoDB value should be provided
	DB JobDatabase

	// Logger represents a user created struct that implements the Logger interface.
	//
	// This logger will be used to log information when managing jobs.
	// If no logger is specified, the library will log nothing.
	Logger Logger

	// MongoDB represents the configuration values that the library need to start a job DB on a mongoDB connection.
	// This configuration uses the original mongoDB driver to do so.
	//
	// When providing this configuration, the library will have access to the user's mongoDB connection,
	// since it will be responsible to manage jobs.
	//
	// If this value is not specified, the DB value should be provided.
	MongoDB *MongoJobDBConfig

	// ProcessingRate represents the rate that the library will process jobs.
	//
	// Defaut: 1 minute
	ProcessingRate time.Duration

	// Location represents the location that the library should use when generating time values.
	//
	// Default: UTC
	Location string

	// DeleteOnDone defines if, when a job is done, the job should be deleted from the database.
	//
	// Default: false
	DeleteOnDone bool

	// DeleteOnCancel defines if, when a job is canceled, the job should be deleted from the database.
	//
	// Default: false
	DeleteOnCancel bool
}

Config represents the configuration for the go-scheduler lib

type Finder

type Finder struct {
	Status string
	Name   string
	Data   map[string]any
}

type Job

type Job struct {
	// ID its the job ID in the database
	ID string

	// ScheduleType represents the job schedule type, if its a job that runs only once (SIMPLE) or if its a job that runs recurrently (RECURRENT)
	ScheduleType

	// Status represents the job schedule current status, if its pending, failed, done, or canceled
	Status ScheduleStatus

	// NextRunAt defines when the job should run
	NextRunAt time.Time

	// LastRunAt defines when the job was last ran
	LastRunAt *time.Time

	// ScheduleString its the schedule string that was defined when the job schedule was configured,
	// when its a RECURRENT schedule
	ScheduleString string

	// ScheduleLimitDate defines the limit date that the RECURRENT job will run.
	//
	// When the limit date arrives, the job is set as DONE.
	// If no limit date is set, the job will run forever until its manually canceled on deleted.
	ScheduleLimitDate *time.Time

	// Name represents the job definition name
	Name string

	// Data represents the extra data that the user can provide when defining a job
	Data map[string]any
}

Job represents a schedule job.

func List

func List(f Finder) ([]*Job, error)

List lists jobs on the database given the finder.

func (*Job) Cancel

func (j *Job) Cancel() error

Cancel sets the job schedule status as CANCELED and saves it on the database

func (*Job) Delete

func (j *Job) Delete() error

Delete deletes the job from the database

func (*Job) Done

func (j *Job) Done() error

Done sets the job schedule status as DONE and saves it on the database

func (*Job) Fail

func (j *Job) Fail() error

Fail sets the job schedule status as FAILED and saves it on the database

func (*Job) HasFailed

func (j *Job) HasFailed() bool

HasFailed returns true if the job status is FAILED, and false otherwise

func (*Job) IsCanceled

func (j *Job) IsCanceled() bool

IsCanceled returns true if the job status is CANCELED, and false otherwise

func (*Job) IsDone

func (j *Job) IsDone() bool

IsDone returns true if the job status is DONE, and false otherwise

func (*Job) IsPending

func (j *Job) IsPending() bool

IsPending returns true if the job status is PENDING, and false otherwise

func (*Job) IsRecurrent

func (j *Job) IsRecurrent() bool

IsRecurrent return true if the job schedule type is RECURRENT, and false otherwise

func (*Job) IsSimple

func (j *Job) IsSimple() bool

IsSimple return true if the job schedule type is SIMPLE, and false otherwise

type JobDatabase

type JobDatabase interface {
	// InitJobDB its a function that will be called at the beggining of the library instantiation.
	//
	// It should start the job database an make it ready to read and write jobs.
	InitJobDB() error

	// ListExpiredSchedules should list jobs that are ready to run
	ListExpiredSchedules() ([]*Job, error)

	// List should list jobs given the Finder
	List(f Finder) ([]*Job, error)

	// SaveJob should save a job in its current state on the job database
	//
	// It should receive a job struct, and "upsert" it in the database. (If it's a new job, should insert a new job, if its an existent job, should update the existent job).
	SaveJob(j Job) error

	// DeleteJob should delete a job completely from the database
	DeleteJob(j Job) error
}

JobDatabase represents a database that can manipulate job documents

type JobFunc

type JobFunc func(*Job) (err error)

JobFunc represents a function that can handle jobs

type Logger

type Logger interface {
	// Error logs a message on error level
	Error(message string)

	// Errorf formats a message according to a format specifier and logs the message on error level
	Errorf(format string, a ...any)
}

Logger defines a go-scheduler logger

type MongoJobDBConfig

type MongoJobDBConfig struct {
	// Conn its the mongo db connection that the user can provide.
	Conn *mongo.Client

	// DbName its the database name that the library should use to save jobs
	//
	// Default: go-scheduler
	DbName string

	// CollName its the collection name that the library should use to save jobs
	//
	// Default: scheduler-jobs
	CollName string
}

type ScheduleStatus

type ScheduleStatus string

func (ScheduleStatus) String

func (t ScheduleStatus) String() string

String returns the schedule status in string notation

type ScheduleType

type ScheduleType string

func (ScheduleType) String

func (t ScheduleType) String() string

String returns the schedule type in string notation

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL