/* * 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 com.aliyun.odps.graph; import java.io.IOException; import java.util.ArrayList; import java.util.List; import com.aliyun.odps.io.Writable; import com.aliyun.odps.io.WritableComparable; /** * Vertex 表示图的一个点,封装了 ODPS Graph 作业的主要计算逻辑,图作业都需要提供 Vertex 实现类. * * <p> * 一个点包括: * <ul> * <li>标识 id,自定义,继承自 {@link WritableComparable} ,通常要求全局唯一,{@link Partitioner} 根据 * id 决定由哪个 Worker 负责; * <li>值 value,自定义,继承自 {@link Writable}; * <li>状态 halt,布尔值,表示该点是否结束; * <li>边集 edges,以该点为起始点的所有边列表; * </ul> * 在迭代执行过程中,ODPS Graph 框架会按一定频率做 checkpoint,上述内容将被持久化并在 failover * 时被恢复,除此之外的其他信息,用户需要在 {@linkplain #setup(WorkerContext) setup} 函数中进行初始化。 * </p> * * <p> * Vertex 是抽象类,通常继承实现下面的方法: * <ul> * <li>{@link #setup(WorkerContext) setup} 默认实现为空逻辑,在迭代开始时会被调用,通常在此方法里对点进行初始化; * <li>{@link #compute(ComputeContext, Iterable) compute} * 抽象方法,必须实现,在每轮迭代中,如果点为非结束状态或者收到上一轮迭代发送的消息,则此方法会被调用; * <li>{@linkplain #cleanup(WorkerContext) cleanup} 默认实现为空逻辑,在迭代结束后会被调用; * </ul> * </p> * * @param <I> * Vertex ID 类型 * @param <V> * Vertex Value 类型 * @param <E> * Edge Value 类型 * @param <M> * Message 类型 * @see GraphLoader * @see Aggregator * @see WorkerContext */ @SuppressWarnings("rawtypes") public abstract class Vertex<I extends WritableComparable, V extends Writable, E extends Writable, M extends Writable> { private I id; private V value; private boolean halt; private List<Edge<I, E>> edges = null; /** * 设置 Vertex ID,要求继承自 {@link WritableComparable}. * <p> * 在 * {@linkplain GraphLoader#load(com.aliyun.odps.io.LongWritable, com.aliyun.odps.Record, Vertex)} * 中可以调用此方法为点设置 ID * </p> * * @param id * Vertex ID */ public final void setId(I id) { this.id = id; } /** * 获取 Vertex ID. * * @return 返回 Vertex ID */ public final I getId() { return id; } /** * 获取点的取值,自定义,继承自 {@link Writable}; * * @return 点的取值 */ public final V getValue() { return value; } /** * 设置点的取值,自定义,继承自 {@link Writable}; * * <p> * 在 * {@linkplain GraphLoader#load(LongWritable recordNum, WritableRecord record, * MutationContext<VERTEX_ID, VERTEX_VALUE, EDGE_VALUE, MESSAGE> context)} * 中可以调用此方法为点设置值 * </p> * * @param value * 点的取值 */ public final void setValue(V value) { this.value = value; } /** * 设置点为结束状态. * * <p> * 本方法通常在 {@link #compute(ComputeContext, Iterable)} * 中调用,如果在一轮迭代中调用此方法将点设置为结束状态后: * <ul> * <li>如果该点收到消息,则下一轮迭代时框架会调用 {@link #wakeUp()} 自动唤醒该点,并调用该点的 * {@link #compute(ComputeContext, Iterable)} 方法; * <li>如果该点未收到消息,则下一轮迭代时略过调用此点的 {@link #compute(ComputeContext, Iterable)} * 方法,直到后续被唤醒; * <ul> * 在一轮迭代后,如果所有点都处于结束状态,并且没有产生任何消息,则迭代终止。 * </p> */ public void voteToHalt() { this.halt = true; } /** * 唤醒此点,通常由框架调用. * * <p> * 如果需要唤醒某点,只需要给点发送消息,在下一轮迭代时,收到消息的点会被自动唤醒。 * </p> */ public void wakeUp() { halt = false; } /** * 查询此点是否处于结束状态. * * @return 若为结束状态,返回true,否则false */ public boolean isHalted() { return halt; } /** * 查询此点是否有出边. * * @return 若边数大于0, 返回true,否则false */ public final boolean hasEdges() { return edges != null && !edges.isEmpty(); } /** * 获取以此点为起始的边集,调用该方法前,建议调用{@link #hasEdges()} 确定此点是否存在出边. * * @return 以此点为起始的边集 */ public final List<Edge<I, E>> getEdges() { return edges; } /** * 获取以此点为起始的边的个数。 * * @return 以此点为起始的边的个数 */ public final int getNumEdges() { return hasEdges() ? getEdges().size() : 0; } /** * 设置以此点为起始的边集,默认为null. * * @param edges * 以此点为起始的边集 */ public final void setEdges(List<Edge<I, E>> edges) { this.edges = edges; } /** * 增加一条边. * * @param destVertexId * 边终点 ID * @param edgeValue * 边值 */ @SuppressWarnings("unchecked") public final void addEdge(I destVertexId, E edgeValue) { if (edges == null) { edges = new ArrayList<Edge<I, E>>(1); } edges.add(new Edge(destVertexId, edgeValue)); } /** * 给定终点,删除对应边,如果有多个相同终点的重复边,也一并会被删除. * * @param destVertexId * 边的终点 ID * @return 成功删除的边集合,若没有找到对应的边,返回空集合 */ public final List<Edge<I, E>> removeEdges(I destVertexId) { List<Edge<I, E>> removeEdges = new ArrayList<Edge<I, E>>(); if (edges != null) { for (Edge<I, E> edge : edges) { if (edge.getDestVertexId().equals(destVertexId)) { removeEdges.add(edge); } } edges.removeAll(removeEdges); } return removeEdges; } /** * 初始化函数,默认实现为空逻辑,在所有迭代开始前调用. * * <p> * 通常在此方法里对点进行初始化,如局部变量。 * * @param context * 上下文信息 * @throws IOException */ public void setup(WorkerContext<I, V, E, M> context) throws IOException { } /** * 计算函数,必须实现此函数. * * <p> * 在每轮迭代中,如果点为非结束状态或者收到上一轮迭代发送的消息,则此方法会被调用,通常在此方法中: * <ul> * <li>处理上一次迭代发给当前点的消息; * <li>根据需要对图进行编辑:1)修改点/边的取值;2)发送消息给某些点;3)增加/删除点或边; * <li>通过 {@link #aggregate(Object)} 或 {@link #aggregate(int, Object)} * 汇总信息到全局信息; * <li>设置当前点状态,结束或非结束状态; * </ul> * <br/> * 迭代进行过程中,框架会将消息以异步的方式发送到对应 Worker 并在下一轮迭代时进行处理,用户无需关心。 * </p> * * @param context * 上下文信息 * @param messages * 上一次迭代发给当前点的消息 * @throws IOException */ public abstract void compute(ComputeContext<I, V, E, M> context, Iterable<M> messages) throws IOException; /** * 清除函数,默认实现为空逻辑,在所有迭代结束后调用. * * <p> * 通常在此方法里把点的计算结果写到结果表中。 * * @param context * 上下文信息 * @throws IOException */ public void cleanup(WorkerContext<I, V, E, M> context) throws IOException { } /** * 顶点转换成字符串格式. * <p> * 字符串格式如下: * Vertex(id=<顶点id>,value=<顶点value>,#edgesNum=<顶点出边数量>) * 注意: * 如果顶点id和顶点value是自定义类型,需要重写其toString()方法 * </p> * * @return 顶点转换的字符串格式 */ @Override public String toString() { return "Vertex(id=" + getId() + ",value=" + getValue() + ",#edgesNum=" + getNumEdges() + ")"; } }