monsteraexample

package module
v0.0.0-...-7ae1893 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2025 License: MIT Imports: 21 Imported by: 0

README

Monstera Example

Go Go Report Card

An example of how to build applications with Monstera framework. This is an imaginary multi-tenant SaaS for distributed RW locks. Basically, this is a simplified version of Everblack Grackle service, with locks only, trivial account management, and no authentication.

Here is a bare minimum of docs you must read before jumping into this codebase:

Monstera framework does not force any particular application core implementation, method routing mechanism, or any specific wire format. It is up to you to define that. However, over time I developed a certain style of how all Everblack services are implemented. To separate a clean part of the framework from that opinionated part I made two packages: github.com/evrblk/monstera for the core part, and github.com/evrblk/monstera/x for the rest. However, a lot of things are not generalizable or extractable into a library. And this example application shows how all of them can be assembled together.

Applications cores

There are 3 application cores:

  • AccountsCore in accounts.go
  • NamespacesCore in namespaces.go
  • LocksCore in locks.go

All these cores are implemented in my opinionated way and serve as an example of how it can be done. You are free to do it any way you want, with different in-memory data structures or other embedded databases.

Application cores store data in BadgerDB. There is one instance of BadgerDB per process, so multiple shards and multiple cores share it. To avoid conflicts, each table is prefixed with table IDs (in tables.go). Each shard has its own boundaries (lowerBound and upperBound). Take a look how keys are built for tables and indexes (typically in the bottom of files with application cores and inside monstera/x package too).

All core data structures are defined in protobufs in corepb/*. Those structures are exposed from Monstera stubs and used by application cores to store data in BadgerDB. corepb/cloud.proto has high level containers for requests and responses that are actually passed by Monstera. Monstera does not know anything about implementation of your application cores and only passes binary blobs as requests and responses for reads and updates. Message routing to a binary blob and from a blob is based on oneof protobuf structure (see adapters.go and stubs.go).

Take a look at tests (accounts_test.go, locks_test.go and namespaces_test.go). Application cores are easily testable without any mocks, and even very complex business logic can be tested by feeding the correct seqeunce of commands since all application cores are state machines without side effects.

Gateway server

A gateway (or frontend) server is the public API part of the system. In this example it serves gRPC, but it can be anything you want (OpenAPI, ConnectRPC, gRPC, Gin, etc). Gateway gRPC is defined in gatewaypb/*. Protos are not shared between gateway and core parts for clean separation of core business layer and presentation layer. The code for converting between them lives in pbconv.go. server.go is the implementation of the gateway API. It is the entry point for all user actions, and if you want to trace and understand the lifecycle of a request then start from here.

Gateway server is the place to do:

  • Authentication
  • Authorization (not in this example)
  • Validations
  • Throttling (not in this example)

Gateway server communicates with Monstera cluster via monstera.MonsteraClient. All Monstera operations are deterministic, so the gateway is the place to generate random numbers or get the current time before sending a core request to Monstera cluster.

This example is relatively simple and all operations from gateway API map 1-to-1 to core operations (not including authentication). However, in more complex applications a single gateway operation can collect or update data in multiple application cores (Everblack Bison and Eveblack Moab has such operations, for example).

Here authentication is dumb. An account id is passed in headers and the server picks it up without any actual check. Just for demonstration purposes. In real Everblack Cloud it is implemented as described here, and evrblk/evrblk-go/authn package is used internally.

Executables

The whole application consists of two parts:

  • cmd/gateway - a stateless web server with public API. This is basically a runner for the Gateway gRPC server from above.
  • cmd/node - stateful Monstera node with all the data and business logic. This is a runner for monstera.MonsteraServer and the place to register all implementations of your application cores.

Each Monstera node has 2 BadgerDB stores: one for all application cores, and one for all Raft logs from all shards on that node.

Procfile has 3 Monstera nodes and 1 gateway server configured. Use goreman to start:

go tool github.com/mattn/goreman start

There is also a standalone executable cmd/standalone that runs both parts of the application in a single Go process, non-sharded and non-replicated. Read more about standalone applications here. To run a standalone app:

go run ./cmd/standalone --port=8000 --data-dir=./data/standalone

Monstera codegen

Monstera codegen is the opinionated part of the framework. I wanted to achieve type-safety and utilize comple-time checks without reflection, but wanted to eliminate human mistakes from vast boilerplate code. So I generate all boilerplate code.

monstera.yaml defines all application cores and their operations. generator.go has an annotation for running //go:generate for Monstera codegen. It produces:

  • api.go with interfaces for application cores and stubs
  • adapters.go with adapter to application cores, that turns binary blobs into routable requests
  • stubs.go with service stubs, that turn requests into binary blobs and route them to the correct application core

Monstera codegen relies on several conventions in order to make it work in a type-safe way:

  • Methods of application core must have corresponding *Response and *Request objects in go_code.corepb_package package. For example, AcquireLock of Locks core must have AcquireLockRequest and AcquireLockResponse proto messages in github.com/evrblk/monstera-example/corepb.
  • *Response and *Request objects must be included into oneof of corresponding high level containers update_request_proto, update_response_proto, etc. For example, AcquireLockRequest must be included into UpdateRequest, AcquireLockResponse must be included into UpdateResponse.

The reason why I do not generate high level containers (in corepb/cloud.proto) is because of protobuf field tags. They need to be consistent and never change. That means I would need to assign field tags right in the yaml file, which I did not like. If I find an elegant and safe way to do it, I will simplify this codegen part.

sharding.go has an implementation of a shard key calculator. I chose not to use annotations or reflection to extract shard keys from requests. Instead, Monstera codegen generates a simple interface where every method corresponds to a *Request object. You specify explicitly how to extract a shard key from each request with one line of Go code.

Cluster config

Cluster config is used by MonsteraClient. There is already one generated for you in cluster_config.pb. cluster_config.json is a human-readable version of the same config, check it out.

Current cluster config has:

  • 3 nodes
  • 16 shards of Namespaces
  • 16 shards of Locks
  • 1 shard of Accounts
  • 3 replicas of each

To print a json version of any config run:

go tool github.com/evrblk/monstera/cmd/monstera cluster print-config --monstera-config=./cluster_config.pb

You can seed a new config. Keep in mind, if you run this command it will regenerate the config with new random ids and you will also need to update Procfile with that new ids:

go run ./cmd/dev seed-monstera-cluster

How to run

  1. Clone this repository.

  2. Make sure it builds:

go build -v ./...
  1. Start a cluster with 3 nodes and a gateway server:
go tool github.com/mattn/goreman start
  1. Create 100 accounts:
go run ./cmd/dev seed-accounts
  1. Pick any account id from previous step output.

  2. Run a test scenario 1 which creates a namespace and tries to grab a lock with the account id:

go run ./cmd/dev scenario-1 --account-id=9fff3bf7d1f9561d

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidId = errors.New("invalid id")
)

Functions

func DecodeAccountId

func DecodeAccountId(s string) (uint64, error)

func EncodeAccountId

func EncodeAccountId(id uint64) string

Types

type AccountsCore

type AccountsCore struct {
	// contains filtered or unexported fields
}

func NewAccountsCore

func NewAccountsCore(badgerStore *monstera.BadgerStore) *AccountsCore

func (*AccountsCore) Close

func (c *AccountsCore) Close()

func (*AccountsCore) CreateAccount

func (*AccountsCore) DeleteAccount

func (*AccountsCore) GetAccount

func (*AccountsCore) ListAccounts

func (*AccountsCore) Restore

func (c *AccountsCore) Restore(reader io.ReadCloser) error

func (*AccountsCore) Snapshot

func (*AccountsCore) UpdateAccount

type AccountsCoreAdapter

type AccountsCoreAdapter struct {
	// contains filtered or unexported fields
}

func NewAccountsCoreAdapter

func NewAccountsCoreAdapter(accountsCore AccountsCoreApi) *AccountsCoreAdapter

func (*AccountsCoreAdapter) Close

func (a *AccountsCoreAdapter) Close()

func (*AccountsCoreAdapter) Read

func (a *AccountsCoreAdapter) Read(request []byte) []byte

func (*AccountsCoreAdapter) Restore

func (a *AccountsCoreAdapter) Restore(r io.ReadCloser) error

func (*AccountsCoreAdapter) Snapshot

func (*AccountsCoreAdapter) Update

func (a *AccountsCoreAdapter) Update(request []byte) []byte

type AccountsCoreApi

type AccountsCoreApi interface {
	Snapshot() monstera.ApplicationCoreSnapshot
	Restore(reader io.ReadCloser) error
	Close()
	ListAccounts(request *corepb.ListAccountsRequest) (*corepb.ListAccountsResponse, error)
	GetAccount(request *corepb.GetAccountRequest) (*corepb.GetAccountResponse, error)
	CreateAccount(request *corepb.CreateAccountRequest) (*corepb.CreateAccountResponse, error)
	UpdateAccount(request *corepb.UpdateAccountRequest) (*corepb.UpdateAccountResponse, error)
	DeleteAccount(request *corepb.DeleteAccountRequest) (*corepb.DeleteAccountResponse, error)
}

type AuthenticationMiddleware

type AuthenticationMiddleware struct {
}

func (*AuthenticationMiddleware) Unary

func (m *AuthenticationMiddleware) Unary(
	ctx context.Context,
	req interface{},
	info *grpc.UnaryServerInfo,
	handler grpc.UnaryHandler,
) (interface{}, error)

type ExampleServiceApiServer

type ExampleServiceApiServer struct {
	gatewaypb.UnimplementedExampleServiceApiServer
	// contains filtered or unexported fields
}

func NewExampleServiceApiServer

func NewExampleServiceApiServer(coreApiClient ExampleServiceCoreApi) *ExampleServiceApiServer

func (*ExampleServiceApiServer) AcquireLock

func (*ExampleServiceApiServer) Close

func (s *ExampleServiceApiServer) Close()

func (*ExampleServiceApiServer) CreateNamespace

func (*ExampleServiceApiServer) DeleteLock

func (*ExampleServiceApiServer) DeleteNamespace

func (*ExampleServiceApiServer) GetLock

func (*ExampleServiceApiServer) GetNamespace

func (*ExampleServiceApiServer) ListNamespaces

func (*ExampleServiceApiServer) ReleaseLock

func (*ExampleServiceApiServer) UpdateNamespace

type ExampleServiceCoreApi

type ExampleServiceCoreApi interface {
	ListAccounts(ctx context.Context, request *corepb.ListAccountsRequest) (*corepb.ListAccountsResponse, error)
	GetAccount(ctx context.Context, request *corepb.GetAccountRequest) (*corepb.GetAccountResponse, error)
	CreateAccount(ctx context.Context, request *corepb.CreateAccountRequest) (*corepb.CreateAccountResponse, error)
	UpdateAccount(ctx context.Context, request *corepb.UpdateAccountRequest) (*corepb.UpdateAccountResponse, error)
	DeleteAccount(ctx context.Context, request *corepb.DeleteAccountRequest) (*corepb.DeleteAccountResponse, error)

	GetNamespace(ctx context.Context, request *corepb.GetNamespaceRequest) (*corepb.GetNamespaceResponse, error)
	ListNamespaces(ctx context.Context, request *corepb.ListNamespacesRequest) (*corepb.ListNamespacesResponse, error)
	CreateNamespace(ctx context.Context, request *corepb.CreateNamespaceRequest) (*corepb.CreateNamespaceResponse, error)
	UpdateNamespace(ctx context.Context, request *corepb.UpdateNamespaceRequest) (*corepb.UpdateNamespaceResponse, error)
	DeleteNamespace(ctx context.Context, request *corepb.DeleteNamespaceRequest) (*corepb.DeleteNamespaceResponse, error)

	AcquireLock(ctx context.Context, request *corepb.AcquireLockRequest) (*corepb.AcquireLockResponse, error)
	ReleaseLock(ctx context.Context, request *corepb.ReleaseLockRequest) (*corepb.ReleaseLockResponse, error)
	DeleteLock(ctx context.Context, request *corepb.DeleteLockRequest) (*corepb.DeleteLockResponse, error)
	GetLock(ctx context.Context, request *corepb.GetLockRequest) (*corepb.GetLockResponse, error)
}

type ExampleServiceCoreApiMonsteraStub

type ExampleServiceCoreApiMonsteraStub struct {
	// contains filtered or unexported fields
}

func NewExampleServiceCoreApiMonsteraStub

func NewExampleServiceCoreApiMonsteraStub(monsteraClient *monstera.MonsteraClient, shardKeyCalculator ExampleServiceMonsteraShardKeyCalculator) *ExampleServiceCoreApiMonsteraStub

func (*ExampleServiceCoreApiMonsteraStub) AcquireLock

func (*ExampleServiceCoreApiMonsteraStub) CreateAccount

func (*ExampleServiceCoreApiMonsteraStub) CreateNamespace

func (*ExampleServiceCoreApiMonsteraStub) DeleteAccount

func (*ExampleServiceCoreApiMonsteraStub) DeleteLock

func (*ExampleServiceCoreApiMonsteraStub) DeleteNamespace

func (*ExampleServiceCoreApiMonsteraStub) GetAccount

func (*ExampleServiceCoreApiMonsteraStub) GetLock

func (*ExampleServiceCoreApiMonsteraStub) GetNamespace

func (*ExampleServiceCoreApiMonsteraStub) ListAccounts

func (*ExampleServiceCoreApiMonsteraStub) ListNamespaces

func (*ExampleServiceCoreApiMonsteraStub) ReleaseLock

func (*ExampleServiceCoreApiMonsteraStub) UpdateAccount

func (*ExampleServiceCoreApiMonsteraStub) UpdateNamespace

type ExampleServiceCoreApiStandaloneStub

type ExampleServiceCoreApiStandaloneStub struct {
	// contains filtered or unexported fields
}

func NewExampleServiceCoreApiStandaloneStub

func NewExampleServiceCoreApiStandaloneStub(accountsCore AccountsCoreApi, namespacesCore NamespacesCoreApi, locksCore LocksCoreApi) *ExampleServiceCoreApiStandaloneStub

func (*ExampleServiceCoreApiStandaloneStub) AcquireLock

func (*ExampleServiceCoreApiStandaloneStub) CreateAccount

func (*ExampleServiceCoreApiStandaloneStub) CreateNamespace

func (*ExampleServiceCoreApiStandaloneStub) DeleteAccount

func (*ExampleServiceCoreApiStandaloneStub) DeleteLock

func (*ExampleServiceCoreApiStandaloneStub) DeleteNamespace

func (*ExampleServiceCoreApiStandaloneStub) GetAccount

func (*ExampleServiceCoreApiStandaloneStub) GetLock

func (*ExampleServiceCoreApiStandaloneStub) GetNamespace

func (*ExampleServiceCoreApiStandaloneStub) ListAccounts

func (*ExampleServiceCoreApiStandaloneStub) ListNamespaces

func (*ExampleServiceCoreApiStandaloneStub) ReleaseLock

func (*ExampleServiceCoreApiStandaloneStub) UpdateAccount

func (*ExampleServiceCoreApiStandaloneStub) UpdateNamespace

type ExampleServiceMonsteraShardKeyCalculator

type ExampleServiceMonsteraShardKeyCalculator interface {
	ListAccountsShardKey(request *corepb.ListAccountsRequest) []byte
	GetAccountShardKey(request *corepb.GetAccountRequest) []byte
	CreateAccountShardKey(request *corepb.CreateAccountRequest) []byte
	UpdateAccountShardKey(request *corepb.UpdateAccountRequest) []byte
	DeleteAccountShardKey(request *corepb.DeleteAccountRequest) []byte

	GetNamespaceShardKey(request *corepb.GetNamespaceRequest) []byte
	ListNamespacesShardKey(request *corepb.ListNamespacesRequest) []byte
	CreateNamespaceShardKey(request *corepb.CreateNamespaceRequest) []byte
	UpdateNamespaceShardKey(request *corepb.UpdateNamespaceRequest) []byte
	DeleteNamespaceShardKey(request *corepb.DeleteNamespaceRequest) []byte

	AcquireLockShardKey(request *corepb.AcquireLockRequest) []byte
	ReleaseLockShardKey(request *corepb.ReleaseLockRequest) []byte
	DeleteLockShardKey(request *corepb.DeleteLockRequest) []byte
	GetLockShardKey(request *corepb.GetLockRequest) []byte
}

type LocksCore

type LocksCore struct {
	// contains filtered or unexported fields
}

func NewLocksCore

func NewLocksCore(badgerStore *monstera.BadgerStore, shardLowerBound []byte, shardUpperBound []byte) *LocksCore

func (*LocksCore) AcquireLock

func (c *LocksCore) AcquireLock(request *corepb.AcquireLockRequest) (*corepb.AcquireLockResponse, error)

func (*LocksCore) Close

func (c *LocksCore) Close()

func (*LocksCore) DeleteLock

func (c *LocksCore) DeleteLock(request *corepb.DeleteLockRequest) (*corepb.DeleteLockResponse, error)

func (*LocksCore) GetLock

func (c *LocksCore) GetLock(request *corepb.GetLockRequest) (*corepb.GetLockResponse, error)

func (*LocksCore) ReleaseLock

func (c *LocksCore) ReleaseLock(request *corepb.ReleaseLockRequest) (*corepb.ReleaseLockResponse, error)

func (*LocksCore) Restore

func (c *LocksCore) Restore(reader io.ReadCloser) error

func (*LocksCore) Snapshot

type LocksCoreAdapter

type LocksCoreAdapter struct {
	// contains filtered or unexported fields
}

func NewLocksCoreAdapter

func NewLocksCoreAdapter(locksCore LocksCoreApi) *LocksCoreAdapter

func (*LocksCoreAdapter) Close

func (a *LocksCoreAdapter) Close()

func (*LocksCoreAdapter) Read

func (a *LocksCoreAdapter) Read(request []byte) []byte

func (*LocksCoreAdapter) Restore

func (a *LocksCoreAdapter) Restore(r io.ReadCloser) error

func (*LocksCoreAdapter) Snapshot

func (*LocksCoreAdapter) Update

func (a *LocksCoreAdapter) Update(request []byte) []byte

type LocksCoreApi

type LocksCoreApi interface {
	Snapshot() monstera.ApplicationCoreSnapshot
	Restore(reader io.ReadCloser) error
	Close()
	AcquireLock(request *corepb.AcquireLockRequest) (*corepb.AcquireLockResponse, error)
	ReleaseLock(request *corepb.ReleaseLockRequest) (*corepb.ReleaseLockResponse, error)
	DeleteLock(request *corepb.DeleteLockRequest) (*corepb.DeleteLockResponse, error)
	GetLock(request *corepb.GetLockRequest) (*corepb.GetLockResponse, error)
}

type NamespacesCore

type NamespacesCore struct {
	// contains filtered or unexported fields
}

func NewNamespacesCore

func NewNamespacesCore(badgerStore *monstera.BadgerStore, shardLowerBound []byte, shardUpperBound []byte) *NamespacesCore

func (*NamespacesCore) Close

func (c *NamespacesCore) Close()

func (*NamespacesCore) CreateNamespace

func (*NamespacesCore) DeleteNamespace

func (*NamespacesCore) GetNamespace

func (*NamespacesCore) ListNamespaces

func (*NamespacesCore) Restore

func (c *NamespacesCore) Restore(reader io.ReadCloser) error

func (*NamespacesCore) Snapshot

func (*NamespacesCore) UpdateNamespace

type NamespacesCoreAdapter

type NamespacesCoreAdapter struct {
	// contains filtered or unexported fields
}

func NewNamespacesCoreAdapter

func NewNamespacesCoreAdapter(namespacesCore NamespacesCoreApi) *NamespacesCoreAdapter

func (*NamespacesCoreAdapter) Close

func (a *NamespacesCoreAdapter) Close()

func (*NamespacesCoreAdapter) Read

func (a *NamespacesCoreAdapter) Read(request []byte) []byte

func (*NamespacesCoreAdapter) Restore

func (a *NamespacesCoreAdapter) Restore(r io.ReadCloser) error

func (*NamespacesCoreAdapter) Snapshot

func (*NamespacesCoreAdapter) Update

func (a *NamespacesCoreAdapter) Update(request []byte) []byte

type NamespacesCoreApi

type NamespacesCoreApi interface {
	Snapshot() monstera.ApplicationCoreSnapshot
	Restore(reader io.ReadCloser) error
	Close()
	GetNamespace(request *corepb.GetNamespaceRequest) (*corepb.GetNamespaceResponse, error)
	ListNamespaces(request *corepb.ListNamespacesRequest) (*corepb.ListNamespacesResponse, error)
	CreateNamespace(request *corepb.CreateNamespaceRequest) (*corepb.CreateNamespaceResponse, error)
	UpdateNamespace(request *corepb.UpdateNamespaceRequest) (*corepb.UpdateNamespaceResponse, error)
	DeleteNamespace(request *corepb.DeleteNamespaceRequest) (*corepb.DeleteNamespaceResponse, error)
}

type ShardKeyCalculator

type ShardKeyCalculator struct{}

func (*ShardKeyCalculator) AcquireLockShardKey

func (g *ShardKeyCalculator) AcquireLockShardKey(request *corepb.AcquireLockRequest) []byte

func (*ShardKeyCalculator) CreateAccountShardKey

func (g *ShardKeyCalculator) CreateAccountShardKey(request *corepb.CreateAccountRequest) []byte

func (*ShardKeyCalculator) CreateNamespaceShardKey

func (g *ShardKeyCalculator) CreateNamespaceShardKey(request *corepb.CreateNamespaceRequest) []byte

func (*ShardKeyCalculator) DeleteAccountShardKey

func (g *ShardKeyCalculator) DeleteAccountShardKey(request *corepb.DeleteAccountRequest) []byte

func (*ShardKeyCalculator) DeleteLockShardKey

func (g *ShardKeyCalculator) DeleteLockShardKey(request *corepb.DeleteLockRequest) []byte

func (*ShardKeyCalculator) DeleteNamespaceShardKey

func (g *ShardKeyCalculator) DeleteNamespaceShardKey(request *corepb.DeleteNamespaceRequest) []byte

func (*ShardKeyCalculator) GetAccountShardKey

func (g *ShardKeyCalculator) GetAccountShardKey(request *corepb.GetAccountRequest) []byte

func (*ShardKeyCalculator) GetLockShardKey

func (g *ShardKeyCalculator) GetLockShardKey(request *corepb.GetLockRequest) []byte

func (*ShardKeyCalculator) GetNamespaceShardKey

func (g *ShardKeyCalculator) GetNamespaceShardKey(request *corepb.GetNamespaceRequest) []byte

func (*ShardKeyCalculator) ListAccountsShardKey

func (g *ShardKeyCalculator) ListAccountsShardKey(request *corepb.ListAccountsRequest) []byte

func (*ShardKeyCalculator) ListNamespacesShardKey

func (g *ShardKeyCalculator) ListNamespacesShardKey(request *corepb.ListNamespacesRequest) []byte

func (*ShardKeyCalculator) ReleaseLockShardKey

func (g *ShardKeyCalculator) ReleaseLockShardKey(request *corepb.ReleaseLockRequest) []byte

func (*ShardKeyCalculator) UpdateAccountShardKey

func (g *ShardKeyCalculator) UpdateAccountShardKey(request *corepb.UpdateAccountRequest) []byte

func (*ShardKeyCalculator) UpdateNamespaceShardKey

func (g *ShardKeyCalculator) UpdateNamespaceShardKey(request *corepb.UpdateNamespaceRequest) []byte

type UnimplementedExampleServiceCoreApi

type UnimplementedExampleServiceCoreApi struct{}

func (*UnimplementedExampleServiceCoreApi) AcquireLock

func (*UnimplementedExampleServiceCoreApi) CreateAccount

func (*UnimplementedExampleServiceCoreApi) CreateNamespace

func (*UnimplementedExampleServiceCoreApi) DeleteAccount

func (*UnimplementedExampleServiceCoreApi) DeleteLock

func (*UnimplementedExampleServiceCoreApi) DeleteNamespace

func (*UnimplementedExampleServiceCoreApi) GetAccount

func (*UnimplementedExampleServiceCoreApi) GetLock

func (*UnimplementedExampleServiceCoreApi) GetNamespace

func (*UnimplementedExampleServiceCoreApi) ListAccounts

func (*UnimplementedExampleServiceCoreApi) ListNamespaces

func (*UnimplementedExampleServiceCoreApi) ReleaseLock

func (*UnimplementedExampleServiceCoreApi) UpdateAccount

func (*UnimplementedExampleServiceCoreApi) UpdateNamespace

Directories

Path Synopsis
cmd
dev command
gateway command
node command
standalone command
dlocks module
ledger module
tinyurl module

Jump to

Keyboard shortcuts

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