/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ignite.spi.eventstorage.memory;
import java.util.Collection;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.events.Event;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgnitePredicate;
import org.apache.ignite.resources.LoggerResource;
import org.apache.ignite.spi.IgniteSpiAdapter;
import org.apache.ignite.spi.IgniteSpiConfiguration;
import org.apache.ignite.spi.IgniteSpiException;
import org.apache.ignite.spi.IgniteSpiMBeanAdapter;
import org.apache.ignite.spi.IgniteSpiMultipleInstancesSupport;
import org.apache.ignite.spi.eventstorage.EventStorageSpi;
import org.jsr166.ConcurrentLinkedDeque8;
import static org.apache.ignite.events.EventType.EVT_NODE_METRICS_UPDATED;
/**
* In-memory {@link org.apache.ignite.spi.eventstorage.EventStorageSpi} implementation. All events are
* kept in the FIFO queue. If no configuration is provided a default expiration
* {@link #DFLT_EXPIRE_AGE_MS} and default count {@link #DFLT_EXPIRE_COUNT} will
* be used.
* <p>
* It's recommended not to set huge size and unlimited TTL because this might
* lead to consuming a lot of memory and result in {@link OutOfMemoryError}.
* Both event expiration time and maximum queue size could be changed at
* runtime.
* <p>
* <h1 class="header">Configuration</h1>
* <h2 class="header">Mandatory</h2>
* This SPI has no mandatory configuration parameters.
* <h2 class="header">Optional</h2>
* The following configuration parameters are optional:
* <ul>
* <li>Event queue size (see {@link #setExpireCount(long)})</li>
* <li>Event time-to-live value (see {@link #setExpireAgeMs(long)})</li>
* <li>{@link #setFilter(org.apache.ignite.lang.IgnitePredicate)} - Event filter that should be used for decision to accept event.</li>
* </ul>
* <h2 class="header">Java Example</h2>
* MemoryEventStorageSpi is used by default and should be explicitly configured only
* if some SPI configuration parameters need to be overridden. Examples below insert own
* events queue size value that differs from default 10000.
* <pre name="code" class="java">
* MemoryEventStorageSpi = new MemoryEventStorageSpi();
*
* // Init own events size.
* spi.setExpireCount(2000);
*
* IgniteConfiguration cfg = new IgniteConfiguration();
*
* // Override default event storage SPI.
* cfg.setEventStorageSpi(spi);
*
* // Starts grid.
* G.start(cfg);
* </pre>
* <h2 class="header">Spring Example</h2>
* MemoryEventStorageSpi can be configured from Spring XML configuration file:
* <pre name="code" class="xml">
* <bean id="grid.custom.cfg" class="org.apache.ignite.configuration.IgniteConfiguration" singleton="true">
* ...
* <property name="discoverySpi">
* <bean class="org.apache.ignite.spi.eventStorage.memory.MemoryEventStorageSpi">
* <property name="expireCount" value="2000"/>
* </bean>
* </property>
* ...
* </bean>
* </pre>
* <p>
* <img src="http://ignite.apache.org/images/spring-small.png">
* <br>
* For information about Spring framework visit <a href="http://www.springframework.org/">www.springframework.org</a>
* @see org.apache.ignite.spi.eventstorage.EventStorageSpi
*/
@IgniteSpiMultipleInstancesSupport(true)
public class MemoryEventStorageSpi extends IgniteSpiAdapter implements EventStorageSpi {
/** Default event time to live value in milliseconds (value is {@link Long#MAX_VALUE}). */
public static final long DFLT_EXPIRE_AGE_MS = Long.MAX_VALUE;
/** Default expire count (value is {@code 10000}). */
public static final int DFLT_EXPIRE_COUNT = 10000;
/** */
@LoggerResource
private IgniteLogger log;
/** Event time-to-live value in milliseconds. */
private long expireAgeMs = DFLT_EXPIRE_AGE_MS;
/** Maximum queue size. */
private long expireCnt = DFLT_EXPIRE_COUNT;
/** Events queue. */
private ConcurrentLinkedDeque8<Event> evts = new ConcurrentLinkedDeque8<>();
/** Configured event predicate filter. */
private IgnitePredicate<Event> filter;
/**
* Gets filter for events to be recorded.
*
* @return Filter to use.
*/
public IgnitePredicate<Event> getFilter() {
return filter;
}
/**
* Sets filter for events to be recorded.
*
* @param filter Filter to use.
* @return {@code this} for chaining.
*/
@IgniteSpiConfiguration(optional = true)
public MemoryEventStorageSpi setFilter(IgnitePredicate<Event> filter) {
this.filter = filter;
return this;
}
/** {@inheritDoc} */
@Override public void spiStart(String igniteInstanceName) throws IgniteSpiException {
// Start SPI start stopwatch.
startStopwatch();
assertParameter(expireCnt > 0, "expireCnt > 0");
assertParameter(expireAgeMs > 0, "expireAgeMs > 0");
// Ack parameters.
if (log.isDebugEnabled()) {
log.debug(configInfo("expireAgeMs", expireAgeMs));
log.debug(configInfo("expireCnt", expireCnt));
}
registerMBean(igniteInstanceName, new MemoryEventStorageSpiMBeanImpl(this), MemoryEventStorageSpiMBean.class);
// Ack ok start.
if (log.isDebugEnabled())
log.debug(startInfo());
}
/** {@inheritDoc} */
@Override public void spiStop() throws IgniteSpiException {
unregisterMBean();
// Reset events.
evts.clear();
// Ack ok stop.
if (log.isDebugEnabled())
log.debug(stopInfo());
}
/**
* See {@link #setExpireAgeMs(long)}
*
* @return Event time-to-live.
*/
public long getExpireAgeMs() {
return expireAgeMs;
}
/**
* Sets events expiration time. All events that exceed this value
* will be removed from the queue when next event comes.
* <p>
* If not provided, default value is {@link #DFLT_EXPIRE_AGE_MS}.
*
* @param expireAgeMs Expiration time in milliseconds.
* @return {@code this} for chaining.
*/
@IgniteSpiConfiguration(optional = true)
public MemoryEventStorageSpi setExpireAgeMs(long expireAgeMs) {
this.expireAgeMs = expireAgeMs;
return this;
}
/**
* See {@link #setExpireCount(long)}
*
* @return Maximum event queue size.
*/
public long getExpireCount() {
return expireCnt;
}
/**
* Sets events queue size. Events will be filtered out when new request comes.
* <p>
* If not provided, default value {@link #DFLT_EXPIRE_COUNT} will be used.
*
* @param expireCnt Maximum queue size.
* @return {@code this} for chaining.
*/
@IgniteSpiConfiguration(optional = true)
public MemoryEventStorageSpi setExpireCount(long expireCnt) {
this.expireCnt = expireCnt;
return this;
}
/**
* Gets current queue size of the event queue.
*
* @return Current queue size of the event queue.
*/
public long getQueueSize() {
return evts.sizex();
}
/**
* Removes all events from the event queue.
*/
public void clearAll() {
evts.clear();
}
/** {@inheritDoc} */
@Override public <T extends Event> Collection<T> localEvents(IgnitePredicate<T> p) {
A.notNull(p, "p");
cleanupQueue();
return F.retain((Collection<T>)evts, true, p);
}
/** {@inheritDoc} */
@Override public void record(Event evt) throws IgniteSpiException {
assert evt != null;
// Filter out events.
if (filter == null || filter.apply(evt)) {
cleanupQueue();
evts.add(evt);
// Make sure to filter out metrics updates to prevent log from flooding.
if (evt.type() != EVT_NODE_METRICS_UPDATED && log.isDebugEnabled())
log.debug("Event recorded: " + evt);
}
}
/**
* Method cleans up all events that either outnumber queue size
* or exceeds time-to-live value. It does none if someone else
* cleans up queue (lock is locked) or if there are queue readers
* (readersNum > 0).
*/
private void cleanupQueue() {
long now = U.currentTimeMillis();
long queueOversize = evts.sizex() - expireCnt;
for (int i = 0; i < queueOversize && evts.sizex() > expireCnt; i++) {
Event expired = evts.poll();
if (log.isDebugEnabled())
log.debug("Event expired by count: " + expired);
}
while (true) {
ConcurrentLinkedDeque8.Node<Event> node = evts.peekx();
if (node == null) // Queue is empty.
break;
Event evt = node.item();
if (evt == null) // Competing with another thread.
continue;
if (now - evt.timestamp() < expireAgeMs)
break;
if (evts.unlinkx(node) && log.isDebugEnabled())
log.debug("Event expired by age: " + node.item());
}
}
/** {@inheritDoc} */
@Override public MemoryEventStorageSpi setName(String name) {
super.setName(name);
return this;
}
/** {@inheritDoc} */
@Override public String toString() {
return S.toString(MemoryEventStorageSpi.class, this);
}
/**
* MBean implementation for MemoryEventStorageSpi.
*/
private class MemoryEventStorageSpiMBeanImpl extends IgniteSpiMBeanAdapter implements MemoryEventStorageSpiMBean {
/** {@inheritDoc} */
MemoryEventStorageSpiMBeanImpl(IgniteSpiAdapter spiAdapter) {
super(spiAdapter);
}
/** {@inheritDoc} */
@Override public long getExpireAgeMs() {
return MemoryEventStorageSpi.this.getExpireAgeMs();
}
/** {@inheritDoc} */
@Override public long getExpireCount() {
return MemoryEventStorageSpi.this.getExpireCount();
}
/** {@inheritDoc} */
@Override public long getQueueSize() {
return MemoryEventStorageSpi.this.getQueueSize();
}
/** {@inheritDoc} */
@Override public void clearAll() {
MemoryEventStorageSpi.this.clearAll();
}
}
}