 ac97fcd764
			
		
	
	ac97fcd764
	
	
	
		
			
			server.c: Added new member to SERVER->master_err_is_logged. It is used if server loses master status. It causes error log print in readwritesplit router's eror handling. Initial value is false and it is set always to false when server's status is set to master. Added message log printing to mysql monitor, if master status changes to something else. It is not warning or error but only information which probably interests the user. readwritesplit.c:Muted warnings and error printings in cases if slaves are not found if it is allowed to have a master only. readwritesplit.c:Corrected error log printing in case where master lost its status. REdundant prints are removed.
		
			
				
	
	
		
			578 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			578 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * This file is distributed as part of the MariaDB Corporation MaxScale.  It is free
 | |
|  * software: you can redistribute it and/or modify it under the terms of the
 | |
|  * GNU General Public License as published by the Free Software Foundation,
 | |
|  * version 2.
 | |
|  *
 | |
|  * This program is distributed in the hope that it will be useful, but WITHOUT
 | |
|  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 | |
|  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 | |
|  * details.
 | |
|  *
 | |
|  * You should have received a copy of the GNU General Public License along with
 | |
|  * this program; if not, write to the Free Software Foundation, Inc., 51
 | |
|  * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 | |
|  *
 | |
|  * Copyright MariaDB Corporation Ab 2013-2014
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * @file server.c  - A representation of a backend server within the gateway.
 | |
|  *
 | |
|  * @verbatim
 | |
|  * Revision History
 | |
|  *
 | |
|  * Date		Who			Description
 | |
|  * 18/06/13	Mark Riddoch		Initial implementation
 | |
|  * 17/05/14	Mark Riddoch		Addition of unique_name
 | |
|  * 20/05/14	Massimiliano Pinto	Addition of server_string
 | |
|  * 21/05/14	Massimiliano Pinto	Addition of node_id
 | |
|  * 28/05/14	Massimiliano Pinto	Addition of rlagd and node_ts fields
 | |
|  * 20/06/14	Massimiliano Pinto	Addition of master_id, depth, slaves fields
 | |
|  * 26/06/14	Mark Riddoch		Addition of server parameters
 | |
|  * 30/08/14	Massimiliano Pinto	Addition of new service status description 
 | |
|  * 30/10/14	Massimiliano Pinto	Addition of SERVER_MASTER_STICKINESS description
 | |
|  *
 | |
|  * @endverbatim
 | |
|  */
 | |
| #include <stdio.h>
 | |
| #include <stdlib.h>
 | |
| #include <string.h>
 | |
| #include <session.h>
 | |
| #include <server.h>
 | |
| #include <spinlock.h>
 | |
| #include <dcb.h>
 | |
| #include <skygw_utils.h>
 | |
| #include <log_manager.h>
 | |
| 
 | |
| /** Defined in log_manager.cc */
 | |
| extern int            lm_enabled_logfiles_bitmask;
 | |
| extern size_t         log_ses_count[];
 | |
| extern __thread log_info_t tls_log_info;
 | |
| 
 | |
| static SPINLOCK	server_spin = SPINLOCK_INIT;
 | |
| static SERVER	*allServers = NULL;
 | |
| 
 | |
| /**
 | |
|  * Allocate a new server withn the gateway
 | |
|  *
 | |
|  *
 | |
|  * @param servname	The server name
 | |
|  * @param protocol	The protocol to use to connect to the server
 | |
|  * @param port		The port to connect to
 | |
|  *
 | |
|  * @return		The newly created server or NULL if an error occured
 | |
|  */
 | |
| SERVER *
 | |
| server_alloc(char *servname, char *protocol, unsigned short port)
 | |
| {
 | |
| SERVER 	*server;
 | |
| 
 | |
| 	if ((server = (SERVER *)calloc(1, sizeof(SERVER))) == NULL)
 | |
| 		return NULL;
 | |
| 	server->name = strdup(servname);
 | |
| 	server->protocol = strdup(protocol);
 | |
| 	server->port = port;
 | |
| 	server->status = SERVER_RUNNING;
 | |
| 	server->node_id = -1;
 | |
| 	server->rlag = -2;
 | |
| 	server->master_id = -1;
 | |
| 	server->depth = -1;
 | |
| 
 | |
| 	spinlock_acquire(&server_spin);
 | |
| 	server->next = allServers;
 | |
| 	allServers = server;
 | |
| 	spinlock_release(&server_spin);
 | |
| 
 | |
| 	return server;
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Deallocate the specified server
 | |
|  *
 | |
|  * @param server	The service to deallocate
 | |
|  * @return	Returns true if the server was freed
 | |
|  */
 | |
| int
 | |
| server_free(SERVER *server)
 | |
| {
 | |
| SERVER *ptr;
 | |
| 
 | |
| 	/* First of all remove from the linked list */
 | |
| 	spinlock_acquire(&server_spin);
 | |
| 	if (allServers == server)
 | |
| 	{
 | |
| 		allServers = server->next;
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		ptr = allServers;
 | |
| 		while (ptr && ptr->next != server)
 | |
| 		{
 | |
| 			ptr = ptr->next;
 | |
| 		}
 | |
| 		if (ptr)
 | |
| 			ptr->next = server->next;
 | |
| 	}
 | |
| 	spinlock_release(&server_spin);
 | |
| 
 | |
| 	/* Clean up session and free the memory */
 | |
| 	free(server->name);
 | |
| 	free(server->protocol);
 | |
| 	if (server->unique_name)
 | |
| 		free(server->unique_name);
 | |
| 	if (server->server_string)
 | |
| 		free(server->server_string);
 | |
| 	free(server);
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Set a unique name for the server
 | |
|  *
 | |
|  * @param	server	The server to set the name on
 | |
|  * @param	name	The unique name for the server
 | |
|  */
 | |
| void
 | |
| server_set_unique_name(SERVER *server, char *name)
 | |
| {
 | |
| 	server->unique_name = strdup(name);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Find an existing server using the unique section name in
 | |
|  * configuration file
 | |
|  *
 | |
|  * @param	servname	The Server name or address
 | |
|  * @param	port		The server port
 | |
|  * @return	The server or NULL if not found
 | |
|  */
 | |
| SERVER *
 | |
| server_find_by_unique_name(char *name)
 | |
| {
 | |
| SERVER 	*server;
 | |
| 
 | |
| 	spinlock_acquire(&server_spin);
 | |
| 	server = allServers;
 | |
| 	while (server)
 | |
| 	{
 | |
| 		if (server->unique_name && strcmp(server->unique_name, name) == 0)
 | |
| 			break;
 | |
| 		server = server->next;
 | |
| 	}
 | |
| 	spinlock_release(&server_spin);
 | |
| 	return server;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Find an existing server
 | |
|  *
 | |
|  * @param	servname	The Server name or address
 | |
|  * @param	port		The server port
 | |
|  * @return	The server or NULL if not found
 | |
|  */
 | |
| SERVER *
 | |
| server_find(char *servname, unsigned short port)
 | |
| {
 | |
| SERVER 	*server;
 | |
| 
 | |
| 	spinlock_acquire(&server_spin);
 | |
| 	server = allServers;
 | |
| 	while (server)
 | |
| 	{
 | |
| 		if (strcmp(server->name, servname) == 0 && server->port == port)
 | |
| 			break;
 | |
| 		server = server->next;
 | |
| 	}
 | |
| 	spinlock_release(&server_spin);
 | |
| 	return server;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Print details of an individual server
 | |
|  *
 | |
|  * @param server	Server to print
 | |
|  */
 | |
| void
 | |
| printServer(SERVER *server)
 | |
| {
 | |
| 	printf("Server %p\n", server);
 | |
| 	printf("\tServer:			%s\n", server->name);
 | |
| 	printf("\tProtocol:		%s\n", server->protocol);
 | |
| 	printf("\tPort:			%d\n", server->port);
 | |
| 	printf("\tTotal connections:	%d\n", server->stats.n_connections);
 | |
| 	printf("\tCurrent connections:	%d\n", server->stats.n_current);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Print all servers
 | |
|  *
 | |
|  * Designed to be called within a debugger session in order
 | |
|  * to display all active servers within the gateway
 | |
|  */
 | |
| void
 | |
| printAllServers()
 | |
| {
 | |
| SERVER	*ptr;
 | |
| 
 | |
| 	spinlock_acquire(&server_spin);
 | |
| 	ptr = allServers;
 | |
| 	while (ptr)
 | |
| 	{
 | |
| 		printServer(ptr);
 | |
| 		ptr = ptr->next;
 | |
| 	}
 | |
| 	spinlock_release(&server_spin);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Print all servers to a DCB
 | |
|  *
 | |
|  * Designed to be called within a debugger session in order
 | |
|  * to display all active servers within the gateway
 | |
|  */
 | |
| void
 | |
| dprintAllServers(DCB *dcb)
 | |
| {
 | |
| SERVER	*ptr;
 | |
| char	*stat;
 | |
| 
 | |
| 	spinlock_acquire(&server_spin);
 | |
| 	ptr = allServers;
 | |
| 	while (ptr)
 | |
| 	{
 | |
| 		dcb_printf(dcb, "Server %p (%s)\n", ptr, ptr->unique_name);
 | |
| 		dcb_printf(dcb, "\tServer:				%s\n",
 | |
| 								ptr->name);
 | |
| 		stat = server_status(ptr);
 | |
| 		dcb_printf(dcb, "\tStatus:               		%s\n",
 | |
| 									stat);
 | |
| 		free(stat);
 | |
| 		dcb_printf(dcb, "\tProtocol:			%s\n",
 | |
| 								ptr->protocol);
 | |
| 		dcb_printf(dcb, "\tPort:				%d\n",
 | |
| 								ptr->port);
 | |
| 		if (ptr->server_string)
 | |
| 			dcb_printf(dcb, "\tServer Version:\t\t\t%s\n",
 | |
| 							ptr->server_string);
 | |
| 		dcb_printf(dcb, "\tNode Id:			%d\n",
 | |
| 								ptr->node_id);
 | |
| 		dcb_printf(dcb, "\tMaster Id:			%d\n",
 | |
| 								ptr->master_id);
 | |
| 		if (ptr->slaves) {
 | |
| 			int i;
 | |
| 			dcb_printf(dcb, "\tSlave Ids:			");
 | |
| 			for (i = 0; ptr->slaves[i]; i++)
 | |
| 			{
 | |
| 				if (i == 0)
 | |
| 					dcb_printf(dcb, "%li", ptr->slaves[i]);
 | |
| 				else
 | |
| 					dcb_printf(dcb, ", %li ", ptr->slaves[i]);
 | |
| 			}
 | |
| 			dcb_printf(dcb, "\n");
 | |
| 		}
 | |
| 		dcb_printf(dcb, "\tRepl Depth:			%d\n",
 | |
| 							 ptr->depth);
 | |
| 		if (SERVER_IS_SLAVE(ptr) || SERVER_IS_RELAY_SERVER(ptr)) {
 | |
| 			if (ptr->rlag >= 0) {
 | |
| 				dcb_printf(dcb, "\tSlave delay:\t\t%d\n", ptr->rlag);
 | |
| 			}
 | |
| 		}
 | |
| 		if (ptr->node_ts > 0) {
 | |
| 			dcb_printf(dcb, "\tLast Repl Heartbeat:\t%lu\n", ptr->node_ts);
 | |
| 		}
 | |
| 		dcb_printf(dcb, "\tNumber of connections:		%d\n",
 | |
| 						ptr->stats.n_connections);
 | |
| 		dcb_printf(dcb, "\tCurrent no. of conns:		%d\n",
 | |
| 							ptr->stats.n_current);
 | |
|                 dcb_printf(dcb, "\tCurrent no. of operations:	%d\n",
 | |
| 						ptr->stats.n_current_ops);
 | |
|                 ptr = ptr->next;
 | |
| 	}
 | |
| 	spinlock_release(&server_spin);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Print server details to a DCB
 | |
|  *
 | |
|  * Designed to be called within a debugger session in order
 | |
|  * to display all active servers within the gateway
 | |
|  */
 | |
| void
 | |
| dprintServer(DCB *dcb, SERVER *server)
 | |
| {
 | |
| char		*stat;
 | |
| SERVER_PARAM	*param;
 | |
| 
 | |
| 	dcb_printf(dcb, "Server %p (%s)\n", server, server->unique_name);
 | |
| 	dcb_printf(dcb, "\tServer:				%s\n", server->name);
 | |
| 	stat = server_status(server);
 | |
| 	dcb_printf(dcb, "\tStatus:               		%s\n", stat);
 | |
| 	free(stat);
 | |
| 	dcb_printf(dcb, "\tProtocol:			%s\n", server->protocol);
 | |
| 	dcb_printf(dcb, "\tPort:				%d\n", server->port);
 | |
| 	if (server->server_string)
 | |
| 		dcb_printf(dcb, "\tServer Version:\t\t\t%s\n", server->server_string);
 | |
| 	dcb_printf(dcb, "\tNode Id:			%d\n", server->node_id);
 | |
| 	dcb_printf(dcb, "\tMaster Id:			%d\n", server->master_id);
 | |
| 	if (server->slaves) {
 | |
| 		int i;
 | |
| 		dcb_printf(dcb, "\tSlave Ids:			");
 | |
| 		for (i = 0; server->slaves[i]; i++)
 | |
| 		{
 | |
| 			if (i == 0)
 | |
| 				dcb_printf(dcb, "%li", server->slaves[i]);
 | |
| 			else
 | |
| 				dcb_printf(dcb, ", %li ", server->slaves[i]);
 | |
| 		}
 | |
| 		dcb_printf(dcb, "\n");
 | |
| 	}
 | |
| 	dcb_printf(dcb, "\tRepl Depth:			%d\n", server->depth);
 | |
| 	if (SERVER_IS_SLAVE(server) || SERVER_IS_RELAY_SERVER(server)) {
 | |
| 		if (server->rlag >= 0) {
 | |
| 			dcb_printf(dcb, "\tSlave delay:\t\t%d\n", server->rlag);
 | |
| 		}
 | |
| 	}
 | |
| 	if (server->node_ts > 0) {
 | |
| 		struct tm result;
 | |
| 		char 	 buf[40];
 | |
| 		dcb_printf(dcb, "\tLast Repl Heartbeat:\t%s",
 | |
| 			asctime_r(localtime_r((time_t *)(&server->node_ts), &result), buf));
 | |
| 	}
 | |
| 	if ((param = server->parameters) != NULL)
 | |
| 	{
 | |
| 		dcb_printf(dcb, "\tServer Parameters:\n");
 | |
| 		while (param)
 | |
| 		{
 | |
| 			dcb_printf(dcb, "\t\t%-20s\t%s\n", param->name,
 | |
| 								param->value);
 | |
| 			param = param->next;
 | |
| 		}
 | |
| 	}
 | |
| 	dcb_printf(dcb, "\tNumber of connections:		%d\n",
 | |
| 						server->stats.n_connections);
 | |
| 	dcb_printf(dcb, "\tCurrent no. of conns:		%d\n",
 | |
| 						server->stats.n_current);
 | |
|         dcb_printf(dcb, "\tCurrent no. of operations:	%d\n", server->stats.n_current_ops);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * List all servers in a tabular form to a DCB
 | |
|  *
 | |
|  */
 | |
| void
 | |
| dListServers(DCB *dcb)
 | |
| {
 | |
| SERVER	*ptr;
 | |
| char	*stat;
 | |
| 
 | |
| 	spinlock_acquire(&server_spin);
 | |
| 	ptr = allServers;
 | |
| 	if (ptr)
 | |
| 	{
 | |
| 		dcb_printf(dcb, "Servers.\n");
 | |
| 		dcb_printf(dcb, "-------------------+-----------------+-------+-------------+--------------------\n");
 | |
| 		dcb_printf(dcb, "%-18s | %-15s | Port  | Connections | %-20s\n",
 | |
| 			"Server", "Address", "Status");
 | |
| 		dcb_printf(dcb, "-------------------+-----------------+-------+-------------+--------------------\n");
 | |
| 	}
 | |
| 	while (ptr)
 | |
| 	{
 | |
| 		stat = server_status(ptr);
 | |
| 		dcb_printf(dcb, "%-18s | %-15s | %5d | %11d | %s\n",
 | |
| 				ptr->unique_name, ptr->name,
 | |
| 				ptr->port,
 | |
| 				ptr->stats.n_current, stat);
 | |
| 		free(stat);
 | |
| 		ptr = ptr->next;
 | |
| 	}
 | |
| 	if (allServers)
 | |
| 		dcb_printf(dcb, "-------------------+-----------------+-------+-------------+--------------------\n");
 | |
| 	spinlock_release(&server_spin);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Convert a set of  server status flags to a string, the returned
 | |
|  * string has been malloc'd and must be free'd by the caller
 | |
|  *
 | |
|  * @param server The server to return the status of
 | |
|  * @return A string representation of the status flags
 | |
|  */
 | |
| char *
 | |
| server_status(SERVER *server)
 | |
| {
 | |
| char	*status = NULL;
 | |
| 
 | |
| 	if ((status = (char *)malloc(256)) == NULL)
 | |
| 		return NULL;
 | |
| 	status[0] = 0;
 | |
| 	if (server->status & SERVER_MAINT)
 | |
| 		strcat(status, "Maintenance, ");
 | |
| 	if (server->status & SERVER_MASTER)
 | |
| 		strcat(status, "Master, ");
 | |
| 	if (server->status & SERVER_SLAVE)
 | |
| 		strcat(status, "Slave, ");
 | |
| 	if (server->status & SERVER_JOINED)
 | |
| 		strcat(status, "Synced, ");
 | |
| 	if (server->status & SERVER_NDB)
 | |
| 		strcat(status, "NDB, ");
 | |
| 	if (server->status & SERVER_SLAVE_OF_EXTERNAL_MASTER)
 | |
| 		strcat(status, "Slave of External Server, ");
 | |
| 	if (server->status & SERVER_STALE_STATUS)
 | |
| 		strcat(status, "Stale Status, ");
 | |
| 	if (server->status & SERVER_MASTER_STICKINESS)
 | |
| 		strcat(status, "Master Stickiness, ");
 | |
| 	if (server->status & SERVER_AUTH_ERROR)
 | |
| 		strcat(status, "Auth Error, ");
 | |
| 	if (server->status & SERVER_RUNNING)
 | |
| 		strcat(status, "Running");
 | |
| 	else
 | |
| 		strcat(status, "Down");
 | |
| 	return status;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Set a status bit in the server
 | |
|  *
 | |
|  * @param server	The server to update
 | |
|  * @param bit		The bit to set for the server
 | |
|  */
 | |
| void
 | |
| server_set_status(SERVER *server, int bit)
 | |
| {
 | |
| 	server->status |= bit;
 | |
| 	
 | |
| 	/** clear error logged flag before the next failure */
 | |
| 	if (SERVER_IS_MASTER(server)) 
 | |
| 	{
 | |
| 		server->master_err_is_logged = false;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Clear a status bit in the server
 | |
|  *
 | |
|  * @param server	The server to update
 | |
|  * @param bit		The bit to clear for the server
 | |
|  */
 | |
| void
 | |
| server_clear_status(SERVER *server, int bit)
 | |
| {
 | |
| 	server->status &= ~bit;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Add a user name and password to use for monitoring the
 | |
|  * state of the server.
 | |
|  *
 | |
|  * @param server	The server to update
 | |
|  * @param user		The user name to use
 | |
|  * @param passwd	The password of the user
 | |
|  */
 | |
| void
 | |
| serverAddMonUser(SERVER *server, char *user, char *passwd)
 | |
| {
 | |
| 	server->monuser = strdup(user);
 | |
| 	server->monpw = strdup(passwd);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Check and update a server definition following a configuration
 | |
|  * update. Changes will not affect any current connections to this
 | |
|  * server, however all new connections will use the new settings.
 | |
|  *
 | |
|  * If the new settings are different from those already applied to the
 | |
|  * server then a message will be written to the log.
 | |
|  *
 | |
|  * @param server	The server to update
 | |
|  * @param protocol	The new protocol for the server
 | |
|  * @param user		The monitor user for the server
 | |
|  * @param passwd	The password to use for the monitor user
 | |
|  */
 | |
| void
 | |
| server_update(SERVER *server, char *protocol, char *user, char *passwd)
 | |
| {
 | |
| 	if (!strcmp(server->protocol, protocol))
 | |
| 	{
 | |
|                 LOGIF(LM, (skygw_log_write(
 | |
|                         LOGFILE_MESSAGE,
 | |
|                         "Update server protocol for server %s to protocol %s.",
 | |
|                         server->name,
 | |
|                         protocol)));
 | |
| 		free(server->protocol);
 | |
| 		server->protocol = strdup(protocol);
 | |
| 	}
 | |
| 
 | |
|         if (user != NULL && passwd != NULL) {
 | |
|                 if (strcmp(server->monuser, user) == 0 ||
 | |
|                     strcmp(server->monpw, passwd) == 0)
 | |
|                 {
 | |
|                         LOGIF(LM, (skygw_log_write(
 | |
|                                 LOGFILE_MESSAGE,
 | |
|                                 "Update server monitor credentials for server %s",
 | |
| 				server->name)));
 | |
|                         free(server->monuser);
 | |
|                         free(server->monpw);
 | |
|                         serverAddMonUser(server, user, passwd);
 | |
|                 }
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Add a server parameter to a server.
 | |
|  *
 | |
|  * Server parameters may be used by routing to weight the load
 | |
|  * balancing they apply to the server.
 | |
|  *
 | |
|  * @param	server	The server we are adding the parameter to
 | |
|  * @param	name	The parameter name
 | |
|  * @param	value	The parameter value
 | |
|  */
 | |
| void
 | |
| serverAddParameter(SERVER *server, char *name, char *value)
 | |
| {
 | |
| SERVER_PARAM	*param;
 | |
| 
 | |
| 	if ((param = (SERVER_PARAM *)malloc(sizeof(SERVER_PARAM))) == NULL)
 | |
| 	{
 | |
| 		return;
 | |
| 	}
 | |
| 	if ((param->name = strdup(name)) == NULL)
 | |
| 	{
 | |
| 		free(param);
 | |
| 		return;
 | |
| 	}
 | |
| 	if ((param->value = strdup(value)) == NULL)
 | |
| 	{
 | |
| 		free(param->value);
 | |
| 		free(param);
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	param->next = server->parameters;
 | |
| 	server->parameters = param;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Retreive a parameter value from a server
 | |
|  *
 | |
|  * @param server	The server we are looking for a parameter of
 | |
|  * @param name		The name of the parameter we require
 | |
|  * @return	The parameter value or NULL if not found
 | |
|  */
 | |
| char *
 | |
| serverGetParameter(SERVER *server, char *name)
 | |
| {
 | |
| SERVER_PARAM	*param = server->parameters;
 | |
| 
 | |
| 	while (param)
 | |
| 	{
 | |
| 		if (strcmp(param->name, name) == 0)
 | |
| 			return param->value;
 | |
| 		param = param->next;
 | |
| 	}
 | |
| 	return NULL;
 | |
| }
 |